Backoff that works: jitter, caps, and the retry storm
Exponential backoff with a cap, full jitter, and a maximum attempt count. Without the jitter, clients that failed together retry together, and the retry itself becomes the outage.
The retry storm
A provider hiccups for two seconds. Four hundred of your workers get an error at the same instant. Each waits exactly one second and retries. Four hundred requests arrive in the same millisecond, the provider rate limits, everybody waits two seconds, and four hundred requests arrive in the same millisecond again.
The backoff is working exactly as designed and making things worse, because it is deterministic and every client shares the clock.
Full jitter
import random
BASE = 0.5 # seconds
CAP = 30.0 # never wait longer than this
MAX_ATTEMPTS = 6
def delay(attempt: int) -> float:
# Full jitter: a random point in the whole window, not the window
# plus a wobble. Spreads a synchronised herd across the interval
# instead of nudging it.
window = min(CAP, BASE * (2 ** attempt))
return random.uniform(0, window)"Equal jitter" and "decorrelated jitter" both work too. What does not work is delay + random.uniform(0, 0.1), which keeps the herd together and adds a rounding error.
The four parameters
| Parameter | What it controls | Getting it wrong |
|---|---|---|
| Base | How fast the first retry is | Too small and you hammer a service that is already struggling |
| Cap | The longest wait | No cap means a retry scheduled for next Tuesday |
| Max attempts | When to give up | Infinite retries turn a permanent failure into a permanent load |
| Jitter | Whether clients synchronise | None is the storm above |
Retry-After beats your arithmetic
When a 429 carries Retry-After, use it. It is not a suggestion and it is more accurate than any formula, because the server knows when its window resets and you are guessing.
What not to retry
Backoff is for temporary failures. Retrying a permanent one - an invalid recipient, a closed window, a rejected template - is a loop that fails identically until the cap. The full table of which is which is in designing a send queue.
Where to put it
In one place. Retry logic scattered across four call sites will have four different caps and one of them will be missing, and you will find out which during an incident.
Questions people actually ask
Why does exponential backoff need jitter?
Without it, every client that failed at the same moment retries at the same moment. The retries arrive as a synchronised burst, which causes the next failure, which synchronises them harder.
Should I respect Retry-After?
Always, when it is present. It is the server telling you exactly when it will accept you, and ignoring it in favour of your own backoff is a good way to be rate limited for longer.
Njiwa
One inbox for WhatsApp, Facebook and Instagram, and an API that sends over either WhatsApp rail. 7 days free, no card.