Retries and jitter: why a service falls over a second time at the moment it recovers
Retrying looks like the cheapest possible measure and is the simplest way to finish a service off: it adds load exactly when load is hardest to take, and an identical retry schedule gathers a thousand clients into one instant. The lesson takes the mechanism apart on a model: what backoff does about it, what jitter does, and what jitter costs.
Full technical treatment
TL;DR
A retry amplifies load, and an identical schedule synchronises that load into peaks. Every retry is an extra request aimed at a service that is already in trouble; and if every client waits the same amount between attempts, they all come back the same way — in one instant. Hence two measures, and they do different jobs: grow the pause after each failure, so there are fewer retries, and pick that pause at random, so the retries do not coincide.
Hence the main consequence: the service falls over a second time exactly when it recovers. Model: a thousand clients, identical backoff — and the moment the service comes back a peak of 1000 requests arrives in one ten-millisecond step. The same model with full jitter gives a peak of 10, a hundred times lower, while the total number of requests even grows slightly — 6906 against 6000. Jitter changes the schedule, not the amount of work.
Beyond that is what separates knowing from having read. A constant interval
is the worst of the three strategies: 20,000 requests during the outage and
not one client served, because the attempts ran out before the service came
back; exponential backoff on the same model sends 5000 instead of 20,000 — four
times fewer. The doubling itself is not a library invention: tcp(7) says six
retries of the initial SYN cover approximately 127 seconds, and 127 is exactly
1 + 2 + 4 + 8 + 16 + 32 + 64. And jitter has a price: in the same model every
client is served by 13,290 ms instead of 3100 — but that is the result of this
model with this random seed rather than a universal "price of jitter": what
reproduces is the direction, not the number. The lesson's numbers are model
numbers throughout: they come from a simulation with explicitly stated
assumptions rather than from measuring a live system — no network delay, instant
refusal, all clients starting at once.
- a request to a service can fail, and the cause is sometimes temporary: a second later the very same request goes through;
- a service can only serve a limited number of requests per unit of time;
- a service has many clients, and they behave identically because the code inside them is the same.
- exponential backoff, full jitter, the thundering herd at the moment of recovery;
- retry budgets, idempotency and idempotency keys.
What is actually being asked
The ladder usually runs like this:
- "What do you do when a request fails?" — the warm-up, where the word "retry" appears.
- "How many times and with what pause?" — where the substance starts.
- "Why exponential backoff rather than a fixed pause?" — a question about load amplification.
- "What is jitter and what is it for?" — the central question.
- "The service was down, came back and fell over again immediately — why?" — the thundering herd.
- "How do you bound retries at the system level?" — a question about retry budgets.
The numbers in this lesson come from a model rather than from a run against a real system. The thundering herd is a property of the retry schedule, and seeing it takes a thousand clients and a reproducible outage. The model gives both; the price is that every conclusion holds exactly as far as the assumptions do, which is why they are named one by one.
Base: why retry at all, and why it is dangerous
Start with the simple part. A request to a service failed. The cause varies, and one of the possible causes is temporary: the service happened to be busy, overloaded, or restarting at that moment. A second later the very same request will go through. So the first thing that comes to mind is to retry — and the thought is a sound one: a retry often does help.
Now add to the picture what is invisible from one client's seat. The service does not have one client. It has a thousand, and the code inside them is the same. When the service stops answering, it is not one request that fails but all of them — and all of them are retried at once.
Two unpleasant things follow, and neither is visible while you look at a single client.
The first: a retry adds load. While the service is not answering it receives not fewer requests than usual but more: to every client's first attempt a second, a third and so on get added. That extra traffic arrives exactly when it is hardest to take.
The second: retries arrive together. If every client waits the same amount between attempts — and they do, because the same code picks the pause — then everyone's next attempt lands on the same instant. A service that has just come up receives a wave rather than a steady stream.
And here is the question this lesson is about: how do you retry so that the retry helps rather than finishes the service off? The answer has two parts, and they answer different questions. Backoff — how much the pause grows after each failure — governs how many retries go into a service that is not answering. Jitter — randomness in choosing the pause — governs when they arrive. Neither substitutes for the other.
That is already enough to answer the basic interview question. Everything below is about how large both effects are in the model and why the tidiest-looking strategy turns out to be the worst.
Mechanism 1: a constant interval is not "the simple option"
The first strategy anyone writes without thinking: retry every hundred milliseconds until it works.
CONSTANT INTERVAL
-----------------
first wave at t=0 (all clients at once) 1000
peak in one 10 ms step after recovery 0
when that peak happened, ms 0
requests sent into the outage (wasted) 20000
total requests sent 20000
clients served 0
all clients served by, ms -1
Two numbers should be read together. 20,000 requests went into a service that could not answer: a thousand clients spent twenty attempts each. And zero served: by the time the service came back, the clients had run out of attempts.
And a third line that is easy to read backwards: the peak after recovery is zero. That is not protection from a peak but its absence for a worse reason: by the time there was somewhere to come back to, nobody was left to come.
That is amplification: during a failure the load on the service that cannot answer does not drop but grows twentyfold — from the thousand first requests to twenty thousand — and it grows exactly when the service is at its worst.
Mechanism 2: exponential backoff is about rate, not about patience
The second strategy doubles the pause after each failure: 100 ms, 200, 400, 800. This is not an invention of application libraries — it is how the kernel itself behaves when retransmitting the packet that opens a connection:
The maximum number of times initial SYNs for an active TCP connection attempt
will be retransmitted … The default value is 6, which corresponds to retrying
for up to approximately 127 seconds.
The doubling is proved in that line by arithmetic: six retries add up to 127 seconds in exactly one case — pauses of 1, 2, 4, 8, 16, 32 and 64. With a constant pause, six retries would fit into a few seconds.
The point of doubling is reducing the rate, not being polite. Each further failure means the previous estimate of the situation was too optimistic, and the right response is to ask less often.
In the model it looks like this:
EXPONENTIAL, NO JITTER
----------------------
first wave at t=0 (all clients at once) 1000
peak in one 10 ms step after recovery 1000
when that peak happened, ms 3100
requests sent into the outage (wasted) 5000
total requests sent 6000
clients served 1000
all clients served by, ms 3100
5000 requests wasted instead of 20,000 — four times fewer. And every client was eventually served.
But look at the line the lesson exists for: the peak after recovery is 1000 requests in a single ten-millisecond step. Everyone waited the same, so everyone came back at the same time. A service that has only just got up receives the whole thousand at once.
Mechanism 3: jitter is about the schedule, not about politeness
The third strategy differs by one line: the pause is chosen at random from zero up to the exponential bound.
EXPONENTIAL + FULL JITTER
-------------------------
first wave at t=0 (all clients at once) 1000
peak in one 10 ms step after recovery 10
when that peak happened, ms 2000
requests sent into the outage (wasted) 5906
total requests sent 6906
clients served 1000
all clients served by, ms 13290
The peak fell from a thousand to ten — a hundredfold. No client backed off: each still retries until it succeeds. What changed is that they stopped doing it at the same time.
Hence the phrasing worth answering with: jitter does not reduce the load, it spreads it. The load is created by the retry itself — jitter only decides at what moment the retries arrive. The total number of requests even grew slightly because of it (6906 against 6000), because random pauses are sometimes shorter than regular ones.
And here is the price usually forgotten in conversation: everyone is served by 13,290 ms instead of 3100. Having spread the peak, we spread the recovery. It is a real trade-off rather than a free improvement: jitter is chosen when the peak is more dangerous than the delay — which is almost always true for a service that falls over under a peak, and not always true for a client that needs an answer soon.
And straight away the boundary, without which that number is dangerous to carry off. 13,290 ms is not "the price of jitter" in general. It is the result of this model under these assumptions: a thousand clients, an outage of exactly two seconds, exponential bounds of 100, 200, 400 and 800 ms, and the random seed 20260905. Change the seed and the number changes; change the length of the outage or the number of clients and it changes too. What reproduces here is the direction: the recovery stretches out, and it stretches further the wider the range the pause is drawn from. So the rule is written from the direction rather than from the number: a peak that has been cut is always paid for by a later "everyone served", and that price is measured in your own system rather than taken from here.
Deeper: what the model does not show
Three things worth saying yourself — and that is part of a good answer too.
Retries are not free for the client making them. Every retry holds a connection and a worker on the client side; with mass retries the client hits its own limits (earlier lessons: descriptors, the accept queue) before the service manages to come back.
Not everything may be retried. A retry is safe for an idempotent operation: a read, a write under a key, a cancellation by identifier. For "take the money" a retry is a second withdrawal, and what is needed there is not a pause but an idempotency key.
A retry budget bounds the system, not the client. A rule like "the share of retries in the total flow has a ceiling" is exactly what the model does not contain: it simulates the behaviour of one layer, while the budget lives at the level of the whole system. Without it every layer of a chain multiplies its neighbour's retries, and three layers with three attempts each turn one request into twenty-seven.
How to answer in an interview
Short answer: retrying amplifies load at the moment of failure, and an identical retry schedule synchronises that load into a peak after recovery. So two different things are needed: exponential backoff, so that there are fewer retries as failures repeat, and jitter, so that clients do not come back together. In this lesson's model jitter cut the peak a hundredfold but pushed the moment everyone is served from 3100 ms out to 13,290.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
Three things separate a good answer. First, you keep two effects apart instead of conflating them: the amount of load is created by the retry itself and reduced by backoff (5000 requests instead of 20,000 in the model), while the shape of the load is set by the schedule and changed by jitter, which takes nothing away (6906 requests against 6000, and a peak of 10 instead of 1000). Second, you mention idempotency: not every operation may be retried, and a pause does not help there. Third, you name the retry budget as a system-level bound, because without it every layer multiplies the previous one's retries.
And one piece of care with the numbers. The price of jitter — "everyone served by 13,290 ms instead of 3100" — is worth quoting, but with the caveat that it is the result of one model under one random seed rather than a constant. The only universal part is the direction: a peak that has been cut is paid for by a later moment at which everyone has been served. Saying 13,290 without that caveat passes one run off as a law.
Next they ask
Why did the service fall over again exactly when it recovered?
Because every client waited the same amount. Exponential backoff without jitter synchronises them: everyone's pauses are 100, 200, 400, 800 — and the next attempt lands on the same instant.
The model shows it directly: the peak after recovery equals the full number of clients, a thousand in one ten-millisecond step. A service that has just come up and is still warming caches and connection pools takes the whole load at once — and falls a second time.
How many attempts should you set?
As many as make sense for this request, not "a few more just in case". In the constant-interval model twenty attempts were spent within two seconds and helped nobody: the clients used them up while the service was still down.
The practical rule follows from the previous lesson's deadline: retry while the request's budget lasts, and no longer. With no budget left, a retry is work whose result nobody needs.
May every operation be retried?
No. A retry is safe only for an idempotent operation: a read, a write under a known key, a cancellation by identifier. For an operation with a side effect — charge, send, credit — a retry means doing it twice.
The cure is not backoff but an idempotency key: the client sends an attempt identifier, the server remembers the result and returns the same answer to a retry instead of acting again. Then retrying becomes safe and everything else in this lesson starts to work.
What is a retry budget?
A ceiling on the share of retries in the total flow. It is needed because backoff and jitter govern one client, while overload is created by their sum.
It matters most in a chain: if each of three layers makes three attempts, one user request becomes twenty-seven requests to the last service. A budget breaks that multiplication; backoff does not.
Common misconceptions
retries do not create load, they only bunch it up
They do create it. Every retry is an extra request, so during a failure the load on the fallen service grows rather than merely being redistributed. The schedule then adds a second effect on top: identical pauses synchronise the clients and gather that amplified load into peaks. In the model a constant interval produced 20,000 requests during a two-second outage with a thousand clients — and not one served, because the attempts ran out before the service came back.
exponential backoff solves the thundering herd
It solves amplification: in the model 5000 requests were wasted instead of 20,000. It does not touch the herd — quite the opposite, it synchronises clients: the peak after recovery in the model equals 1000, that is every client at once.
jitter reduces load
It spreads it. The load is created by the retry itself, and jitter only decides when the retries arrive: the total number of requests in the model even grew slightly — 6906 against 6000 — while the peak after recovery fell from 1000 to 10. What reduces the number of retries is not jitter but backoff: 5000 requests instead of 20,000.
jitter is a free improvement, always switch it on
It has a price, and in this model it is this: every client is served by 13,290 ms with jitter against 3100 ms without. Having spread the peak, you spread the recovery. The number itself is a property of this model and this random seed rather than a constant; what reproduces is the direction. Jitter is chosen when the peak is more dangerous than the delay.
doubling the pause is just a sensible heuristic of application code
The kernel behaves the same way: tcp(7) reports that six retransmissions of the initial SYN cover approximately 127 seconds, a number that only comes out of pauses of 1, 2, 4, 8, 16, 32 and 64. Doubling is a reduction of rate as failures repeat, not politeness.
any request may be retried as long as there is a pause
Only an idempotent one. Retrying an operation with a side effect is a second charge, a second email, a second shipment. A pause changes nothing there; an idempotency key does, letting the server return the earlier result instead of acting again.
if every client has backoff and jitter, the system is protected
Backoff and jitter govern one client, while overload is created by the sum. In a chain of three layers with three attempts each, one user request becomes twenty-seven requests to the last service. Only a retry budget — a system-level rule — breaks that.
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a run of the model rather than being written by hand.
Practice · predict the output
constant = simulate(delay_constant) plain = simulate(delay_exponential) jittered = simulate(delay_full_jitter) print(plain["peak_after"]) print(jittered["peak_after"]) print(constant["served"])
Practice · estimate
Knowledge check
A service was unavailable for two seconds while a thousand clients retried with exponential backoff and no jitter. What happens at the moment of recovery?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A retry amplifies load, and an identical schedule synchronises that load into peaks. Every retry is an extra request aimed at a service that is already in trouble; and if every client waits the same amount between attempts, they all come back the same way — in one instant. Hence two measures, and they do different jobs: grow the pause after each failure, so there are fewer retries, and pick that pause at random, so the retries do not coincide.
- Hence the main consequence: the service falls over a second time exactly when it recovers. Model: a thousand clients, identical backoff — and the moment the service comes back a peak of 1000 requests arrives in one ten-millisecond step. The same model with full jitter gives a peak of 10, a hundred times lower, while the total number of requests even grows slightly — 6906 against 6000. Jitter changes the schedule, not the amount of work.
- Beyond that is what separates knowing from having read. A constant interval is the worst of the three strategies: 20,000 requests during the outage and not one client served, because the attempts ran out before the service came back; exponential backoff on the same model sends 5000 instead of 20,000 — four times fewer. The doubling itself is not a library invention:
tcp(7)says six retries of the initial SYN cover approximately 127 seconds, and 127 is exactly 1 + 2 + 4 + 8 + 16 + 32 + 64. And jitter has a price: in the same model every client is served by 13,290 ms instead of 3100 — but that is the result of this model with this random seed rather than a universal "price of jitter": what reproduces is the direction, not the number. The lesson's numbers are model numbers throughout: they come from a simulation with explicitly stated assumptions rather than from measuring a live system — no network delay, instant refusal, all clients starting at once.
In fact
- They do create it. Every retry is an extra request, so during a failure the load on the fallen service grows rather than merely being redistributed. The schedule then adds a second effect on top: identical pauses synchronise the clients and gather that amplified load into peaks. In the model a constant interval produced 20,000 requests during a two-second outage with a thousand clients — and not one served, because the attempts ran out before the service came back.
- It solves amplification: in the model 5000 requests were wasted instead of 20,000. It does not touch the herd — quite the opposite, it synchronises clients: the peak after recovery in the model equals 1000, that is every client at once.
- It spreads it. The load is created by the retry itself, and jitter only decides when the retries arrive: the total number of requests in the model even grew slightly — 6906 against 6000 — while the peak after recovery fell from 1000 to 10. What reduces the number of retries is not jitter but backoff: 5000 requests instead of 20,000.
- It has a price, and in this model it is this: every client is served by 13,290 ms with jitter against 3100 ms without. Having spread the peak, you spread the recovery. The number itself is a property of this model and this random seed rather than a constant; what reproduces is the direction. Jitter is chosen when the peak is more dangerous than the delay.
- The kernel behaves the same way:
tcp(7)reports that six retransmissions of the initial SYN cover approximately 127 seconds, a number that only comes out of pauses of 1, 2, 4, 8, 16, 32 and 64. Doubling is a reduction of rate as failures repeat, not politeness. - Only an idempotent one. Retrying an operation with a side effect is a second charge, a second email, a second shipment. A pause changes nothing there; an idempotency key does, letting the server return the earlier result instead of acting again.
- Backoff and jitter govern one client, while overload is created by the sum. In a chain of three layers with three attempts each, one user request becomes twenty-seven requests to the last service. Only a retry budget — a system-level rule — breaks that.
What is covered
- What is actually being asked
- Base: why retry at all, and why it is dangerous
- Mechanism 1: a constant interval is not "the simple option"
- Mechanism 2: exponential backoff is about rate, not about patience
- Mechanism 3: jitter is about the schedule, not about politeness
- Deeper: what the model does not show
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
1 SOURCE
- tcp(7), Linux man-pages 6.7Official documentation. Exponential backoff not as advice from an article but as the kernel's own behaviour, in the description of initial SYN retransmission: "The maximum number of times initial SYNs for an active TCP connection attempt will be retransmitted … The default value is 6, which corresponds to retrying for up to approximately 127 seconds". The arithmetic is the proof of doubling: six retries with pauses of 1, 2, 4, 8, 16, 32 and 64 seconds add up to exactly 127.https://man7.org/linux/man-pages/man7/tcp.7.html