Throttling¶
Microsoft Graph and SharePoint both throttle with HTTP 429 and a
Retry-After header; Graph also reports how close an app is to its limit. The
library reads these signals, retries with the server's delay (falling back to
exponential backoff with jitter) and can pace a whole fleet proactively.
Unlike SharePoint's static service limits, Graph throttling is
dynamic — evaluated per scope (per app, per tenant, per app + tenant, per
resource) and per request type — so the library reacts to the signals rather
than enforcing a fixed budget. The quotas below are still declared as data (one
quotas module per product, plus the one row no product owns) so a workload can
be designed from the numbers instead of discovered by being throttled.
Signals¶
| Header | Direction | Meaning |
|---|---|---|
Retry-After |
response | Seconds to wait before retrying a 429/503 |
x-ms-throttle-limit-percentage |
response | Share of the limit consumed (0.8–1.8); >= 1.0 means throttling |
x-ms-resource-unit |
response | The cost of this request (Identity resource units) |
x-ms-throttle-scope |
response | The throttled scope (<Scope>/<Limit>/<AppId>/<TenantId...>) |
x-ms-throttle-information |
response | Why (CPULimitExceeded, WriteLimitExceeded, ...) |
x-ms-throttle-priority |
request | low / normal / high — low is throttled first |
What the library does¶
result = client.users.top(10).get().execute_query_retry() # honor Retry-After + backoff
client.execute_batch(concurrency=5) # retries throttled sub-requests
client.with_rate_limit() # pace a fleet from the signals (proactive)
client.with_rate_limit(per_scope=True) # pace each x-ms-throttle-scope separately
client.with_throttle_priority("low") # mark background work low priority
client.with_rate_limit() reads every response (including batch sub-responses)
and gates the group on Retry-After, Graph's x-ms-throttle-limit-percentage
and SharePoint's health score. When a response reports x-ms-resource-unit, the
proactive pace is scaled by that request cost, so an expensive call rests
proportionally longer. With per_scope=True a throttled x-ms-throttle-scope
pauses only requests tagged with the matching RequestOptions.throttle_scope.
See office365.runtime.http.throttling and office365.runtime.retry.
Best practices to avoid throttling¶
- Honor
Retry-After— it is the server telling you exactly how long to wait; retrying earlier only extends the throttle. - Pace proactively with
with_rate_limit(); mark interactive callswith_throttle_priority("high")and background jobs"low"so low-priority work is throttled first. - Share the load: avoid a large fan-out of concurrent connections; spread requests over time rather than bursting.
- Keep responses small: use
$select/$top/$expanddeliberately; avoid oversized$topand deep expansions that inflate server cost. - Prefer delta queries and change notifications over polling/scanning.
- Batch related operations (JSON batching) and reduce operations per request.
Outlook also caps how many requests a mailbox may have in flight: four at a
time, and four sub-requests per JSON batch — so
execute_batch(concurrency=4)is the ceiling a mailbox workload should ask for (OUTLOOK_QUOTASinoffice365.outlook.quotas). A higherconcurrencydoes not go faster: the surplus queues inside the service. - Watch
X-SharePointHealthScore— when the farm is busy, ease off before it throttles you (the rate limiter already does this). - Set a descriptive
User-Agent(e.g.ISV|Contoso|MyApp/1.0) so Microsoft can attribute and, if needed, raise your limits. - For bulk extraction, use Microsoft Graph Data Connect (not throttled).
Quotas (reference)¶
The numbers below are the limits Microsoft publishes; the first one reached triggers
throttling. A Window of in flight means concurrency rather than a rate. Every row
answers catalog(product=...); the Bound at column names the model that declares
it (most rows are referenced by no model, and are reference data only).
| Product | Limit | Value | Window | Scope | Type | Bound at | Notes |
|---|---|---|---|---|---|---|---|
graph |
any Microsoft Graph request | 130,000 requests | 10s | app | any | — | across all tenants |
cloud-communications |
cloud communications | 50,000 requests | 15s | app+tenant | any | Call | calls |
cloud-communications |
call records | 15,000 requests | 20s | app | any | CallRecord | all tenants; callRecord, participant and session resources |
cloud-communications |
call records | 10,000 requests | 20s | tenant | any | CallRecord | all applications |
cloud-communications |
call records | 1,500 requests | 20s | app+tenant | any | CallRecord | |
cloud-communications |
call records | 40 requests | 20s | call record | any | CallRecord.participants_v2, CallRecord.sessions | sessions, participants and segments of one call record |
cloud-communications |
call records | 40 requests | 20s | collection | any | CallRecordCollection | list call records |
cloud-communications |
PSTN call records | 1,000 requests | 60s | tenant | any | CallRecordCollection.get_direct_routing_calls | directRoutingLogRow, pstnCallLogRow, pstnBlockedUsersLogRow, smsLogRow |
cloud-communications |
PSTN call records | 200 requests | 60s | app+tenant | any | CallRecordCollection.get_direct_routing_calls | |
cloud-communications |
PSTN call records | 50 requests | 60s | collection | any | CallRecordCollection.get_direct_routing_calls | per collection |
cloud-communications |
cloud communications | 10,000 requests | 30s | app+tenant | any | Presence | presence |
cloud-communications |
cloud communications | 750 requests | 30s | app | read | VirtualEvent | virtual events; across all tenants |
cloud-communications |
cloud communications | 15 requests | 30s | app | write | VirtualEvent | virtual events; across all tenants |
directory |
identity | 3,500 resource_units | 10s | app+tenant | any | DirectoryObject, Domain, SubscribedSku | small tenants (<50 users); 5,000 medium (50-500), 8,000 large (>500) |
directory |
identity | 150,000 resource_units | 20s | app | any | DirectoryObject, Domain, SubscribedSku | |
directory |
identity | 3,000 requests | 150s | app+tenant | write | DirectoryObject, Domain, SubscribedSku | |
directory |
identity | 35,000 requests | 300s | app | write | DirectoryObject, Domain, SubscribedSku | |
directory |
identity | 18,000 requests | 300s | tenant | write | DirectoryObject, Domain, SubscribedSku | |
directory |
invitations | 150 requests | 5s | tenant | any | Invitation | any operation on /invitations, all apps |
directory |
identity protection and conditional access | 1 request | 1s | tenant | any | ConditionalAccessPolicy | riskDetection, namedLocation, conditionalAccessPolicy — no Retry-After on 429 |
excel |
excel | 5,000 requests | 10s | app | any | Workbook | all tenants |
excel |
excel | 1,500 requests | 10s | app+tenant | any | Workbook | |
files |
files and lists | 3,000 requests | 300s | user | any | BaseItem, Permission | |
files |
files and lists | 53,687,091,200 bytes | 3600s | user | any | BaseItem, Permission | ingress |
files |
files and lists | 107,374,182,400 bytes | 3600s | user | any | BaseItem, Permission | egress |
files |
files and lists | 1,250 resource_units | 60s | app+tenant | any | BaseItem, Permission | tiers by license count: 1,250 (<1,000 users); 2,500 (<5,000); 3,750 (<15,000); 5,000 (<50,000); 6,250 (50,000+) |
files |
files and lists | 300 requests | 300s | app+tenant | any | BaseItem, Permission | specific sharing APIs |
files |
files and lists | 429,496,729,600 bytes | 3600s | app+tenant | any | BaseItem, Permission | ingress |
files |
files and lists | 429,496,729,600 bytes | 3600s | app+tenant | any | BaseItem, Permission | egress |
files |
files and lists | 3,000 requests | 300s | site | any | BaseItem, Permission | anonymous links |
files |
files and lists | 107,374,182,400 bytes | 7200s | site | any | BaseItem, Permission | anonymous egress (download) |
onenote |
onenote | 120 requests | 60s | app+user | any | — | requests per minute |
onenote |
onenote | 400 requests | 3600s | app+user | any | — | requests per hour |
onenote |
onenote | 5 requests | in flight | app+user | any | — | in flight at any moment |
onenote |
onenote | 240 requests | 60s | app | any | — | requests per minute |
onenote |
onenote | 800 requests | 3600s | app | any | — | requests per hour |
onenote |
onenote | 20 requests | in flight | app | any | — | in flight at any moment |
outlook |
outlook | 10,000 requests | 600s | app+mailbox | any | Calendar, MailFolder, OutlookItem, OutlookUser | v1.0 and beta endpoints |
outlook |
outlook | 4 requests | in flight | app+mailbox | any | Calendar, MailFolder, OutlookItem, OutlookUser | in flight at any moment |
outlook |
outlook | 157,286,400 bytes | 300s | app+mailbox | any | AttachmentCollection | upload (PATCH, POST, PUT) |
outlook |
outlook | 4 requests | in flight | batch | any | GraphClient.execute_batch | sub-requests in flight per JSON batch that addresses Outlook; dependsOn serializes them instead |
reports |
microsoft 365 reports | 14 requests | 600s | app+tenant | any | ReportRoot | CSV, per report API |
reports |
microsoft 365 reports | 40 requests | 600s | tenant | any | ReportRoot | CSV, per report API, all apps |
reports |
microsoft 365 reports | 100 requests | 600s | app+tenant | any | ReportRoot | JSON (beta), per report API |
security |
security | 150 requests | 60s | app+tenant | any | Security | alert, securityActions, secureScore |
security |
security | 10,000 requests | 600s | app+tenant | any | Security | secureScore, secureScorecontrolProfile |
security |
security | 4 requests | in flight | app+tenant | any | Security | secureScore, secureScorecontrolProfile — in flight at any moment |
security |
security | 1,000 requests | 60s | app+tenant | any | — | tiIndicator |
security |
security | 5 requests | 60s | app+tenant | any | CasesRoot | eDiscovery cases |
security |
security | 200 requests | 86400s | tenant | any | AuditCoreRoot.queries | audit log query submissions per rolling 24 hours; baseline, may be higher |
security |
security | 50 requests | in flight | tenant | any | AuditCoreRoot.queries | audit log queries queued or running at any moment |
security |
security | 1,000,000 records | — | query | any | AuditLogQuery | per audit log query; exceeding it ends the query successfully, with isRecordCountLimitExceeded set |
subscriptions |
subscription change | 2,000 requests | 20s | app | write | — | POST, PUT, DELETE, PATCH |
subscriptions |
subscription change | 500 requests | 20s | app+tenant | write | — | POST, PUT, DELETE, PATCH |
subscriptions |
subscription reauthorize | 4,000 requests | 20s | app | write | — | POST /subscriptions/{id}/reauthorize |
subscriptions |
subscription reauthorize | 1,000 requests | 20s | app+tenant | write | — | POST /subscriptions/{id}/reauthorize |
subscriptions |
subscription | 2,000 requests | 20s | app | read | — | GET /subscriptions/{id} |
subscriptions |
subscription | 500 requests | 20s | app+tenant | read | — | GET /subscriptions/{id} |
subscriptions |
subscription list | 40 requests | 20s | app | read | SubscriptionCollection | GET /subscriptions |
subscriptions |
subscription list | 25 requests | 20s | app+tenant | read | SubscriptionCollection | GET /subscriptions |
teams |
microsoft teams | 1,500 requests | 1s | app | read | — | default — any request a resource table does not name |
teams |
microsoft teams | 30 requests | 1s | app+tenant | read | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | read | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | user | read | — | default — any request a resource table does not name |
teams |
microsoft teams | 300 requests | 1s | app | write | — | default — any request a resource table does not name |
teams |
microsoft teams | 30 requests | 1s | app+tenant | write | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | write | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | user | write | — | default — any request a resource table does not name |
teams |
microsoft teams | 150 requests | 1s | app | delete | — | default — any request a resource table does not name |
teams |
microsoft teams | 15 requests | 1s | app+tenant | delete | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | delete | — | default — any request a resource table does not name |
teams |
microsoft teams | 1 request | 1s | user | delete | — | default — any request a resource table does not name |
teams |
microsoft teams | 1,500 requests | 1s | app | read | Team | GET /teams/{id} |
teams |
microsoft teams | 30 requests | 1s | app+tenant | read | Team | GET /teams/{id} |
teams |
microsoft teams | 4 requests | 1s | team, channel or chat | read | Team | GET /teams/{id} |
teams |
microsoft teams | 1,200 requests | 1s | app | read | Team.channels | GET /teams/{id}/channels |
teams |
microsoft teams | 60 requests | 1s | app+tenant | read | Team.channels | GET /teams/{id}/channels |
teams |
microsoft teams | 4 requests | 1s | team, channel or chat | read | Team.channels | GET /teams/{id}/channels |
teams |
microsoft teams | 200 requests | 1s | app | read | Channel.messages | GET /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 20 requests | 1s | app+tenant | read | Channel.messages | GET /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | read | Channel.messages | GET /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 500 requests | 1s | app | write | Channel.messages | POST /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 50 requests | 1s | app+tenant | write | Channel.messages | POST /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | write | Channel.messages | POST /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 1 request | 1s | user | write | Channel.messages | POST /teams/{id}/channels/{id}/messages |
teams |
microsoft teams | 200 requests | 1s | app | read | Chat.messages | GET /chats/{id}/messages |
teams |
microsoft teams | 20 requests | 1s | app+tenant | read | Chat.messages | GET /chats/{id}/messages |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | read | Chat.messages | GET /chats/{id}/messages |
teams |
microsoft teams | 200 requests | 1s | app | write | Chat.messages | POST /chats/{id}/messages |
teams |
microsoft teams | 20 requests | 1s | app+tenant | write | Chat.messages | POST /chats/{id}/messages |
teams |
microsoft teams | 1 request | 1s | team, channel or chat | write | Chat.messages | POST /chats/{id}/messages |
teams |
microsoft teams | 1 request | 1s | user | write | Chat.messages | POST /chats/{id}/messages |