Skip to content

A rate limit backoff strategy that holds up on the busiest registration day

IntegrationsUpdated 2026-08-237 min read

In short

A working backoff strategy has four parts: retry only on responses that can succeed later, wait an exponentially growing interval, randomise that interval so parallel workers do not return together, and obey a Retry-After header whenever the server sends one. Log every rate limited response with its endpoint.

Early bird closes at midnight and 3,100 people register between nine in the morning and three in the afternoon. The sync that pushes registrations into the CRM starts returning 429s at 11:40. The runner retries immediately, gets another 429, retries again, and by half past twelve the queue is 40 minutes deep and the sales team is asking why yesterday's leads have not appeared.

A rate limit backoff strategy is the difference between that afternoon and one where the sync simply runs a little slower. The strategy has four moving parts and most implementations get two of them right.

What a 429 actually tells you

The status code is defined in RFC 6585, written by Nottingham and Fielding in 2012, which describes 429 as indicating that the user has sent too many requests in a given amount of time. Two details in that specification shape how a client should behave.

The response may carry a Retry-After header, and it may not. The word is "may", so a client that assumes the header is present will crash on the vendors that omit it, and a client that ignores the header when present is guessing at a number the server already told it. Both failures are common.

The specification also says 429 responses must not be stored by a cache, and notes that servers are under no obligation to use the status at all. That second point matters more than it looks. A platform under load may answer with a 503, or a 403, or a connection reset, and treat all of them as the same rate limiting signal internally. Your retry logic has to key off behaviour rather than off a single status code, which is where the wider question of reading a vendor's error responses starts.

Why does a naive retry make the outage longer?

Because failure is correlated and so is the retry.

Picture twelve worker threads pushing registration batches. At 11:40 the vendor starts rejecting, and all twelve fail within the same second. Each one sleeps a fixed two seconds and returns. All twelve arrive together. The server, still recovering, rejects all twelve again. They sleep and return together again. The pattern repeats with the arrival rate unchanged, and the only thing your retry logic has achieved is to convert continuous load into synchronised spikes, which is harder for a server to absorb than the original traffic.

Exponential backoff fixes the growth rate. Sleep one second, then two, four, eight, sixteen, thirty two, capped at some ceiling. That reduces the total volume. It does nothing about the synchronisation, because twelve workers that all doubled from the same starting point are still doubling in lockstep. After six attempts they are all sleeping thirty two seconds and all returning at the same instant.

Brooker's 2015 analysis on the AWS architecture blog is the standard treatment and it is short enough to read in one sitting. The proposal is full jitter, where the client sleeps a random interval drawn from zero up to the current ceiling: sleep = random(0, min(cap, base 2 ^ attempt)). Brooker reports that with 100 contending clients this "reduced our call count by more than half" against capped exponential backoff, and that unjittered exponential backoff was the clear loser across the tests. He also describes decorrelated jitter, sleep = min(cap, random(0, sleep 3)), which grows from the previous sleep rather than from an attempt counter.

The arithmetic of spreading twelve workers out

Take the same twelve workers at a thirty two second ceiling. Unjittered, they return in one burst, twelve requests in the same instant, once every thirty two seconds.

With full jitter over the same ceiling, each worker draws uniformly from zero to thirty two seconds. Expected wait is sixteen seconds, so the workers come back roughly twice as fast on average, and the twelve arrivals are spread across a thirty two second span at an average spacing of 32 divided by 12, about 2.7 seconds. The server sees a trickle instead of a wave, and the queue drains faster despite each individual worker waiting a shorter average time. That result feels wrong the first time you see it and it is the entire argument for jitter.

Now size the load itself. Microsoft publishes hard numbers for Dataverse service protection limits: 6,000 requests per user within a five minute sliding window, 1,200 seconds of combined execution time in the same window, and a concurrent request limit of 52 or higher, all enforced per web server (Microsoft, 2026). Six thousand requests per five minutes is twenty per second sustained. Pushing 12,400 registrations one row per request needs 12,400 divided by 6,000, so 2.07 windows, a shade over ten minutes of pure throttle time before any other cost. Batch them 100 at a time and the request count falls to 124, which clears the request ceiling instantly and starts consuming the execution time budget instead. That trade is the actual design decision, and the numbers for your own vendor belong in the throttling post for that platform.

Honour Retry-After, then fall back to your own formula

The precedence order is the part worth writing into the code review checklist.

If the response carries Retry-After, sleep for that duration. Microsoft returns it with every 429 on the Web API and describes the duration as depending on the nature of the requests sent in the preceding five minute period, extending it when the client keeps pushing (Microsoft, 2026). Overriding it with your own shorter interval provokes exactly the extension you were trying to avoid.

If there is no header, use full jitter with a sensible ceiling. Sixty seconds is a reasonable cap for an overnight job. For anything running during show hours, cap lower and fail the batch sooner, because a sync that eventually succeeds at 2am is worthless to a badge desk at 2pm.

Cap the number of attempts as well as the interval, and make the cap a count rather than a total elapsed time, so the behaviour is predictable in the logs. Five attempts with full jitter at a thirty second ceiling gives a worst case of about two and a half minutes per batch, which is the sort of number you can put in a runbook.

What happens after the last attempt deserves a decision rather than a default. Dropping the batch loses registrations. Failing the whole job loses the batches that already succeeded. The version that works is a dead letter queue: write the failed batch, its payload and its idempotency key to a table, alert on the table's row count, and let a separate job drain it once the vendor recovers. That turns a rate limit incident into a number somebody can watch fall to zero, instead of an outage whose recovery depends on someone rerunning the right job with the right date range.

Microsoft's own advice on throughput is worth adopting whatever stack you are on: start with a lower number of requests and gradually increase until you begin to hit the limits, then let the retry duration returned by the server tell you when to send more (Microsoft, 2026). Calculating a safe rate in advance assumes the limit is a constant, and on a shared platform during a busy period it is not.

What should you log for every rate limited response?

Five fields, and the endpoint is the one people forget.

Timestamp, endpoint or operation, the status code returned, the Retry-After value if present, and the attempt number. Without the endpoint you learn that you were throttled and nothing about where, which is useless when three integrations share one credential. With it, you can see within a week that 80 per cent of your 429s come from a single contact search call that runs once per registration and could run once per batch.

Log the count of successful requests too, in the same series. A rate of 400 throttled responses is alarming against 2,000 successes and unremarkable against 200,000. Vendors that publish remaining-quota headers make this easier, and Swapcard exposes cost, used, remaining, limit and reset values on its content API responses (Swapcard, 2026), though the specific budget arithmetic for that platform belongs with its own rate limit post. Microsoft is blunt that its equivalent headers are intended for debugging and that clients should not depend on them to control how many requests they send (Microsoft, 2026), which is good advice to apply generally: read the headers into your logs, and drive behaviour from the responses you actually get.

Where this stops

Backoff manages contention. It cannot create capacity, and there is a class of registration day problem it will not touch.

If your daily volume genuinely exceeds the vendor's daily allowance, no retry schedule fixes it. Backoff just spreads the failure across more hours and makes it harder to see. The signal to watch for is a job whose duration grows every week while its row count grows too; that is a capacity conversation with the vendor, or a redesign to push fewer, larger requests, rather than a tuning exercise.

The second limit is that a retry is only safe if the operation is safe to repeat. A backoff loop wrapped around a non-idempotent write turns one throttled request into several duplicate records, which is a worse outcome than the original 429. Get the write safe first, then make the retry clever.

Start this week by grepping one week of integration logs for 429, grouping by endpoint, and sorting descending. If the top endpoint is one call inside a per-row loop, the cheapest fix available to your integration stack is to hoist it out of the loop, and no amount of jitter will beat that.

Questions people ask about rate limit backoff strategy

What does a 429 response mean?
RFC 6585 defines 429 Too Many Requests as an indication that the user has sent too many requests in a given amount of time. The same specification says the response may include a Retry-After header telling the client how long to wait, and that 429 responses must not be stored by a cache. Servers are not obliged to use the status at all.
Why add jitter to exponential backoff?
Without it, every worker that failed at the same moment retries at the same moment, so the load arrives in synchronised waves. Brooker's 2015 analysis for AWS showed full jitter, sleeping a random interval between zero and the current backoff ceiling, cut the number of calls by more than half in a test with 100 contending clients.
Should you use Retry-After or your own backoff calculation?
Use Retry-After when the server sends it, and fall back to your own exponential calculation when it does not. Microsoft returns a Retry-After header with its 429 responses on Dataverse and advises letting the server tell you how much it can handle rather than calculating a rate yourself. Your own formula is a fallback, never an override.

Related reading

All integrations articles