The usual reason people give for not backing off politely is that it costs time. They are wrong, and it is easy to show.
We gave a client the same job five times — fetch forty resources through an endpoint that permits thirty per minute — and changed only its reaction to a refusal. The strategy that finished first was also, bar one, the most economical. The strategy that hammered came last on every measure that matters, while appearing to win.
The five clients
strategy time requests refused wasted
────────────────────────────────────────────────────
backoff 65.5s 46 6 13.0%
backoff+jitter 99.8s 48 8 16.7%
retry_after 63.3s 51 11 21.6%
reset_aware 60.9s 51 11 21.6%
naive 62.3s 553 513 92.8%
Read the naive row twice. To fetch forty things it issued 553 requests, of which 513 were refused. It spent the throttled minute in a tight loop collecting 429s, and for that it finished 1.4 seconds behind the most patient strategy of the five, and barely a second ahead of the next one.
That is the core finding. Hammering does not buy speed against a fixed window, because nothing you do during the refused minute can shorten it. The window ends when it ends. All the extra requests achieve is load on somebody else's server and a conspicuous entry in their logs.
The server already told you when to come back
The fastest client was the one that read the answer instead of guessing at it. A 429 from this endpoint carries both Retry-After and X-RateLimit-Reset, and sleeping until the reset instant finished the job in 60.9 seconds — ahead of every other strategy, including the hammer.
on 429:
sleep until X-RateLimit-Reset, then resume
It beat exponential backoff by 4.6 seconds because backoff is only an estimate of when to return, and an estimate will overshoot when the server has given you an exact time. A client doubling its wait can easily be asleep for thirty seconds after a window that reopened at twelve. The server knows the answer exactly and publishes it in every refusal, and almost nobody reads it.
Backoff still earned something: it made the fewest requests of any strategy, 46 against 51, because overshooting means fewer probes. If you are optimising for the other party's load rather than your own completion time, that is the trade, and it is a small one either way.
Jitter made everything worse, twice
Standard advice is to add randomness to the backoff so that clients released together do not immediately collide. We measured it and it lost — 99.8 seconds against 65.5 for plain backoff with a single client.
So we tested it the way it is meant to be used, with five concurrent clients:
wall requests refused finish spread
no jitter 62.8s 70 30 0.2s
with jitter 75.7s 83 43 12.2s
Worse again: thirteen more requests, thirteen more refusals, a completion thirteen seconds slower, and clients finishing up to twelve seconds apart instead of together.
The explanation is in the limiter's design, not in the general advice about jitter. The counter keys on the address, so five threads on one machine are not five clients — they are one client with five sockets, sharing a single counter and a single fixed window. There is nothing to decorrelate. Jitter's purpose is to stop independent callers from synchronising on a shared service, and when the callers are already sharing one bucket the random wait is pure delay.
This does not make jitter wrong. It makes it an answer to a question we were not asking, and the same is true for a great many single-tenant scripts that carefully implement it.
What the other end sees
Every one of those 513 refusals was a request somebody's server accepted, routed, matched to a limiter, and refused. A 429 is cheap compared with real work, but it is not free, and at scale the refusals are the load.
It is also what your traffic looks like from the outside. A client averaging nine requests a second against an endpoint that permits one every two seconds is indistinguishable from a scraper, and the response to that is rarely a polite conversation — it is a block at the edge, applied to your address, affecting every other thing you were doing from it.
The 92.8% waste figure is worth carrying around for that reason alone. It is not an efficiency statistic. It is the fraction of your traffic that exists only to be refused — and the fraction an abuse-detection system will notice.
When hammering is actually right
There is one case where it makes some sense: a service with a rolling window that refuses without telling you when to return, so the allowance returns continuously rather than at an instant. There, probing does recover capacity fractionally sooner, because there is no boundary to wait for.
Even then the sensible version is a probe every second or two, not a tight loop. The difference between those two is three orders of magnitude in request count and almost nothing in completion time, which is the same trade as everywhere else in this guide.
What to write
while work remains:
response = request(next_item)
if response.status == 429:
reset = response.headers.get("X-RateLimit-Reset")
if reset:
sleep_until(reset)
else:
sleep(response.headers.get("Retry-After", 5))
continue
consume(response)
Prefer the reset timestamp when it is offered, fall back to Retry-After, and keep a plain exponential backoff for the case where a server refuses you without saying anything — which happens, and is the only situation where guessing is the best available move.
Add jitter only when your clients are genuinely independent — separate machines, separate addresses, separate buckets, all pointed at one service. That is the shape it was designed for.
How this was measured, and what it does not tell you
Forty resources per run through a route limited to thirty per minute, on a Laravel application with a fixed window. Each strategy started from an expired window, and every figure — time, request count, refusals — is counted by the client during the run.
A fixed window flatters the reset-aware client. Knowing exactly when the allowance returns is worth most when it returns all at once. Against a rolling window or a token bucket the allowance trickles back and the gap between reading the reset time and estimating it narrows. The ranking of naive against everything else does not change; the margin between the polite strategies might.
One endpoint, one limiter, one address. These runs cannot say anything about jitter's real purpose, because the setup has no independent clients in it — that is the finding, not a gap we are papering over, but it does mean the jitter numbers are evidence about this shape and not about the technique.
The job was small enough to finish inside two windows. A backlog spanning hours would put more weight on request efficiency than on the few seconds separating these strategies, which is the case where backoff's smaller request count starts to matter more than its overshoot.