Handling 429 Too Many Requests

ยท 1 min read

When an API says you're sending too much, it usually tells you how long to wait. Respect that, slow yourself down, and your integration stops getting throttled.

Most third-party APIs limit how many requests you can make: per second, per minute or per day. Go over and you get 429 Too Many Requests. CRM platforms, payment providers and public APIs all do it, and a nightly sync job is usually the first thing to hit the limit.

Treating a 429 like any other error, or retrying it immediately, makes things worse. You stay over the limit and some APIs extend the penalty if you keep pushing.

Read the Retry-After header

A well-behaved API tells you how long to wait. Retry-After is either a number of seconds or an HTTP date:

HTTP/1.1 429 Too Many Requests
Retry-After: 30

In .NET it's already parsed for you:

if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
    var retryAfter = response.Headers.RetryAfter;
    var wait = retryAfter?.Delta
        ?? (retryAfter?.Date is { } date ? date - DateTimeOffset.UtcNow : TimeSpan.FromSeconds(10));

    await Task.Delay(wait, ct);
}

Some APIs use their own headers instead, such as X-RateLimit-Remaining and X-RateLimit-Reset. Check the documentation for the API you're calling.

Let the resilience handler do it

If you use Microsoft.Extensions.Http.Resilience, its HTTP retry strategy treats 429 as transient and, by default, waits for the Retry-After value when the response includes one (the ShouldRetryAfterHeader option). You get correct behavior from the standard handler without writing the code above:

builder.Services.AddHttpClient<CrmClient>()
    .AddStandardResilienceHandler();

Better: don't hit the limit in the first place

Retrying after a 429 is damage control. If you know the limit, throttle yourself below it. System.Threading.RateLimiting gives you the building blocks:

var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
    TokenLimit = 10,                                  // burst size
    TokensPerPeriod = 10,
    ReplenishmentPeriod = TimeSpan.FromSeconds(1),    // about 10 requests per second
    QueueLimit = 100,
    QueueProcessingOrder = QueueProcessingOrder.OldestFirst
});

using var lease = await limiter.AcquireAsync(1, ct);
if (lease.IsAcquired)
{
    await client.UpdateContactAsync(contact, ct);
}

Share one limiter across every caller of that API in the process, and remember that several instances of your service share the API's limit between them.

Reduce the number of calls

The cheapest request is the one you don't send:

  • Use bulk endpoints. Updating 200 records in one call beats 200 calls.
  • Sync only what changed. Filter by last-modified date instead of re-reading everything.
  • Cache reference data that rarely changes.

Takeaway

When an API returns 429, wait as long as Retry-After says. Better still, throttle yourself below the documented limit, and use bulk and incremental calls so you need fewer requests to begin with.