Microsoft Graph
The package Nall.RateLimiting.Adaptive.MicrosoftGraph connects the limiter to the Microsoft Graph SDK and to SharePoint CSOM. It counts the SharePoint resource units of each request, reads throttled inner responses of a $batch, and puts the limit handler in the right place of the Graph handler pipeline.
dotnet add package Nall.RateLimiting.Adaptive.MicrosoftGraphThe SharePoint budget
Section titled “The SharePoint budget”The facts in this section come from Avoid getting throttled or blocked in SharePoint Online on Microsoft Learn.
- SharePoint Online limits each application in a tenant by resource units (RU) per minute and per 24 hours. The limit depends on the number of licenses of the tenant. Every request of the application counts: Microsoft Graph, CSOM and REST.
- A throttled request gets HTTP 429 or 503, with a
Retry-Afterheader. Throttled requests also count toward the limits. - Microsoft Graph requests have a fixed cost: 1 RU for a single item, a delta with a token, or a file download; 2 RU for a multi-item query or a delta without a token, and for create, update, delete and upload; 5 RU for permission operations, including
$expand=permissions. - The requests in a
$batchare counted one by one. - CSOM and REST have no fixed cost, and they usually cost more than Graph.
Read the page for the limits per tenant size. Microsoft can change the limits and the costs.
Create a Graph client
Section titled “Create a Graph client”var options = SharePointRateLimiter.CreateOptions();options.InitialUnitsPerMinute = 600;options.MaxUnitsPerMinute = 1100;
var limiter = SharePointRateLimiter.Create("sharepoint", options);var handlers = GraphClientHandlers.CreateDefaultHandlers(limiter);var httpClient = GraphClientFactory.Create(handlers);
var credential = new ClientSecretCredential("tenant-id", "client-id", "client-secret");var graph = new GraphServiceClient(httpClient, credential);SharePointRateLimiter.CreateOptions gives options with the SharePoint defaults. SharePointRateLimiter.Create makes a limiter with the $batch throttle classifier. If you do not give options, it uses CreateOptions(). GraphClientHandlers.CreateDefaultHandlers makes the default Graph handlers, and adds an AdaptiveRateLimitHandler with the SharePoint cost estimator.
builder.Services.AddAdaptiveRateLimiter("sharepoint").ForSharePoint();
builder.Services.AddSingleton(sp =>{ var limiter = sp.GetRequiredKeyedService<AdaptiveRateLimiter>("sharepoint"); var handlers = GraphClientHandlers.CreateDefaultHandlers(limiter); var httpClient = GraphClientFactory.Create(handlers); return new GraphServiceClient(httpClient, new DefaultAzureCredential());});ForSharePoint() sets the SharePoint cost estimator, the $batch throttle classifier and the SharePoint defaults on the named limiter. The defaults apply before the configuration binds, so the configuration section and a configure delegate can change them. The estimator applies to clients with AddAdaptiveRateLimitHandler("sharepoint"). A Graph client gets its estimator from CreateDefaultHandlers.
These samples use Microsoft.Graph and Azure.Identity. Use the credential that fits your app.
The SharePoint defaults
Section titled “The SharePoint defaults”SharePoint gives each app 1,250 to 6,250 resource units per minute in a tenant, by the license count of the tenant. In 24 hours, it gives 1,200,000 to 6,000,000. ForSharePoint() and SharePointRateLimiter.CreateOptions() set these defaults:
| Option | Default | Why |
|---|---|---|
InitialUnitsPerMinute |
1500 |
Close to the budget of a small tenant, so that the first minutes cause few throttles. |
MinUnitsPerMinute |
200 |
A $batch of 20 permission requests (100 units) waits 30 seconds at most, well within MaxWait. |
MaxUnitsPerMinute |
5625 |
90% of the per-minute budget of the largest tenants. The rate grows to it only when the server accepts it. |
The other options keep the defaults of AdaptiveRateLimiterOptions.
Set the rates for your tenant
Section titled “Set the rates for your tenant”Set the rates to the limit of your tenant:
- Find the per-app limit per minute for the license count of your tenant on the Microsoft page.
- Set
MaxUnitsPerMinutebelow it, for example to 90% of it. For a job that runs all day, also keep it below the 24-hour limit divided by 1,440 minutes. - Set
InitialUnitsPerMinuteandMinUnitsPerMinuteat or below the maximum. - If other apps or other processes use the same app ID, set the instance count, or lower the maximum.
{ "AdaptiveRateLimiting": { "Limiters": { "sharepoint": { "InitialUnitsPerMinute": 600, "MinUnitsPerMinute": 100, "MaxUnitsPerMinute": 1100 } } }}Handler order
Section titled “Handler order”The Kiota retry handler of the Graph SDK retries throttled responses, and waits for Retry-After. The limit handler must be inside it, so that each retry waits for its units and is counted.
GraphServiceClient └─ ... default Graph handlers └─ RetryHandler retries throttled responses └─ AdaptiveRateLimitHandler waits for units, reads each response └─ ... the other handlers, then the networkGraphClientHandlers.InsertRateLimitHandler puts the handler right after the first RetryHandler, or at the end if there is none. Use it with a handler list of your own:
var handlers = GraphClientFactory.CreateDefaultHandlers();GraphClientHandlers.InsertRateLimitHandler( handlers, new AdaptiveRateLimitHandler(limiter, new SharePointResourceUnitEstimator()));How requests are counted
Section titled “How requests are counted”SharePointResourceUnitEstimator gives each request its RU cost from the method and the URL:
| Request | Cost | Category |
|---|---|---|
A path without a sites, drives, drive or shares segment, for example /me |
0, not limited | graph |
A permissions segment, or permissions in $expand or $select |
Permissions (5) |
sharepoint |
Any method other than GET and HEAD |
List (2) |
sharepoint |
delta with a token or deltatoken |
Get (1) |
sharepoint |
delta without a token |
List (2) |
sharepoint |
Last segment children, items, lists, drives, sites, versions, columns or contentTypes |
List (2) |
sharepoint |
| Any other SharePoint request | Get (1) |
sharepoint |
POST .../$batch |
The sum of the inner requests, with the same rules | sharepoint.batch |
A $batch body that is not valid JSON, or has no requests, costs Get. The estimator reads the body of a $batch and leaves it readable. The categories are on the ratelimit.category tag of the metrics, and in the constants SharePointCategory, BatchCategory and GraphCategory.
Change the costs
Section titled “Change the costs”The costs are in SharePointResourceUnitCosts:
| Property | Type | Default | Meaning |
|---|---|---|---|
Get |
int |
1 |
A request that reads one resource, such as one drive item. |
List |
int |
2 |
A request that reads a collection, such as children or a delta without a token, and each write. |
Permissions |
int |
5 |
A request that reads or changes permissions. |
var costs = new SharePointResourceUnitCosts { Permissions = 10 };
builder.Services.AddAdaptiveRateLimiter("sharepoint").ForSharePoint(costs);For a Graph client, pass the estimator to CreateDefaultHandlers:
var estimator = new SharePointResourceUnitEstimator( new SharePointResourceUnitCosts { Permissions = 10 });var handlers = GraphClientHandlers.CreateDefaultHandlers(limiter, estimator);Throttled $batch responses
Section titled “Throttled $batch responses”Graph answers a $batch with 200, also when it throttles inner requests. The status of each inner request is in the body. GraphBatchThrottleClassifier reads the body of a successful response to a POST to a $batch path. It also does this when the cost category is not sharepoint.batch, for example with a custom cost estimator. The body stays readable for the caller. The classifier uses these rules:
- Each inner response with 429, or 503 with
Retry-After, counts as one throttle. The limiter cuts the rate once, and theratelimit.adaptive.throttledcounter adds the number of throttled inner responses. - The longest inner
Retry-Afterpauses the limiter. - A body that is not valid JSON, or has no
responses, counts as a success.
The retry handler sees 200 and does not send the batch again. Send the throttled inner requests again in your code.
CSOM calls use the same app budget as Graph. SharePointCsom.CreateHandler makes a handler pipeline for CSOM that uses the same limiter:
var handler = SharePointCsom.CreateHandler(limiter);var httpClient = new HttpClient(handler);The pipeline is the Kiota RetryHandler (up to 10 retries), then the limit handler, then a SocketsHttpHandler without cookies. Give it to the HttpClient of your CSOM client in place of its own retry handler. Each call costs SharePointCsom.DefaultCallUnits (2) units with the category csom, because the body of a CSOM call does not tell its real cost. Pass callUnits to change it. The HttpClient disposes the pipeline, but not the limiter.