Microsoft Graph throttling when you manage dozens of tenants

4 min readMarkdown version

Short answer

Graph returns HTTP 429 with a Retry-After header giving the number of seconds to wait, and Microsoft's guidance is to wait that long and retry rather than backing off on your own schedule. Combine requests with JSON batching, which allows up to 20 requests in one call, use $select to ask for fewer fields, and avoid polling patterns in favour of change tracking where it exists. The important detail about batching is that each request inside a batch is evaluated individually against throttling limits, so a batch can return 200 while individual members failed with 429.

What throttling looks like

When a threshold is exceeded, Graph limits further requests from that client for a period, returns HTTP 429 Too Many Requests, and returns a suggested wait time in the response header.

Microsoft is explicit about the correct response: back off using the Retry-After delay, because that is the fastest way to recover, since Graph continues to log resource usage while a client is being throttled. Immediate retries make it worse, and they say so directly: avoid immediate retries, because all requests accrue against your usage limits.

Where the limits apply, and why MSPs hit them differently

Microsoft names the two common causes of throttling: a large number of requests across all applications in a tenant, and a large number of requests from a particular application across all tenants.

That second one is the MSP-specific hazard. A management platform is one application talking to many tenants, so a fleet-wide operation concentrates load on a single application identity in a way that no single-tenant workload ever does. Throttling behaviour also varies by request type: writes are more likely to be throttled than reads, and you can end up in a state where writes are throttled while reads still succeed.

Batching, and the part everyone gets wrong

JSON batching combines multiple requests into one JSON object and one HTTP call. Graph supports up to 20 requests per batch. It saves round trips, which is real and worth having across a fleet.

What it does not do is buy you throttling headroom. Requests in a batch are evaluated individually against the applicable throttling limits, and if any request exceeds the limits it fails with 429 while the batch itself succeeds with 200. A client that only checks the outer status code will silently treat throttled members as successful, which produces a fleet report with quiet holes in it.

  • Check every member status: The outer 200 means the batch was parseable, nothing more. Each response object carries its own status.
  • Retry failed members, not the whole batch: Use each failed member's retry-after value, and consider retrying all failures together after the longest one.
  • SDKs do not save you here: Microsoft notes that while SDKs retry throttled requests automatically when they are not batched, throttled requests inside a batch are not retried automatically.
  • Sequencing has a cost: dependsOn makes requests run in order, and a request whose dependency failed returns 424 Failed Dependency. Microsoft's guidance is that a batch should be either fully sequential or fully parallel.

Request patterns that keep you under the limit

  • Ask for less: $select the fields you use. Default responses carry properties you are going to discard anyway.
  • Stop polling what you can subscribe to: Microsoft calls out continuous polling and repeated collection scans as patterns likely to get an application throttled, and points to change tracking and change notifications instead.
  • Spread work over time: A fleet scan does not have to start at midnight for every tenant at once. Staggering across the window costs nothing and flattens the peak.
  • Respect the proactive signals: Graph returns rate-limit headers describing remaining capacity before you are cut off. Slowing down when they get low is cheaper than recovering from a 429.
  • Separate credentials where it matters: Because limits apply per application across tenants, an application-per-customer model splits the quota pool. It costs onboarding complexity, so it is a trade rather than a default. Our own default is the shared multitenant app, which means partners on the platform draw on one pool; the throttle handling below is what keeps that workable, and a per-customer app registration is a supported alternative.

How rugged.sh handles it

Throttle handling lives in one shared transport rather than in each client, so every call honours Retry-After, respects the proactive rate-limit headers by slowing down before a 429, and treats batched member failures as failures rather than reading only the outer status. The Intune policy read that dominates a fleet-wide governance scan goes through $batch; other reads are individually throttle-aware but not yet batched. It is infrastructure rather than a feature, which is the right place for it.

Questions people ask

What does Microsoft Graph return when you are throttled?
HTTP 429 Too Many Requests, with a Retry-After response header giving the number of seconds to wait. Microsoft's guidance is to wait that many seconds and retry, repeating with the supplied delay until the request succeeds.
How many requests can I put in a Microsoft Graph batch?
Up to 20 individual requests per JSON batch. Each is evaluated separately against throttling limits, so individual members can fail with 429 while the batch itself returns 200.
Does batching avoid Graph throttling?
No. Batching reduces network round trips, not resource usage. Requests inside a batch are evaluated individually against the applicable limits, and Microsoft notes that SDKs do not automatically retry throttled requests that were part of a batch.
Why does my management tool get throttled more than a single-tenant script?
Because one of the two documented throttling causes is a large number of requests from a particular application across all tenants. A multi-tenant platform is one application identity issuing requests for every customer, so fleet-wide operations concentrate load in a way single-tenant workloads never do.

Sources

Microsoft's behaviour described above was read from these pages on the dates shown. Claims that come from our own testing against a live Azure subscription carry that date inline.

Related

Try it on one client

The free tier covers one customer tenant and five users, with no time limit, against your own Azure subscription. Paid plans are priced per tenant, not per user.