Drawing 1 · Interactive
Toggle jitter and watch the retry herd scatter
Attempt 5 of 7
t = 7.87 s
- Attempt 1 at 0 ms: failed.
- Attempt 2 at 461 ms, after waiting 461 ms: failed.
- Attempt 3 at 880 ms, after waiting 419 ms: failed.
- Attempt 4 at 3.11 s, after waiting 2.23 s: failed.
- Attempt 5 at 7.87 s, after waiting 4.75 s: succeeded.
The delay formula · retry n = 3
delayn = min(cap, base × factorn)
= min(30 s, 1 s × 23) = 8 s
sleep = random(0, 8 s) = 4.75 s
Fires at t = 7.87 s, 4.75 s after attempt 4.
24 clients, same outage · retry n = 3
Full jitter spreads their retries across the window: at most 5 land together, so the recovering server is not flattened.
Attempt 5 at 7.87 s succeeded.
What is backoff jitter?
Backoff jitter is the randomness added to the delay between retries. Exponential Backoff decides how long the waits should grow — 1 s, 2 s, 4 s, 8 s, up to a cap — but on its own it keeps every client on the same schedule. If 5,000 sends from one broadcast fail in the same second because of a brief Cloud API error, plain backoff makes all 5,000 retry exactly 1 s later, then 2 s, then 4 s: synchronised spikes that look like a fresh traffic surge to the service trying to recover. Jitter breaks that lockstep.
Three variants are common. Full jitter picks a uniformly random wait between zero and the exponential delay: sleep = random(0, min(cap, base × 2^n)). Equal jitter keeps half of the delay and randomises the other half: t = min(cap, base × 2^n), then sleep = t/2 + random(0, t/2), which guarantees a minimum wait. Decorrelated jitter grows from the previous sleep rather than the attempt number: sleep = min(cap, random(base, previous × 3)). A widely cited simulation of many clients competing for one resource found no jitter worst by a wide margin, equal jitter doing slightly more work and taking clearly longer than the other two, and full jitter doing the least total work while decorrelated jitter finished slightly sooner.
Jitter is the randomisation layer; exponential backoff is the underlying schedule, and the Retry Policy decides whether a failure should be retried at all. Jitter matters most where many clients share one failure — a Broadcast hitting Error 130429 (Rate Limit Hit) because it exceeded the number's throughput, a fleet of workers reconnecting after a deploy, or thousands of webhook handlers calling a CRM that has just come back. That synchronised surge is the Thundering Herd, and jitter is its cheapest cure.
Key takeaways
- Backoff jitter adds randomness to each retry delay so clients that failed at the same moment spread their retries out.
- Full jitter waits random(0, min(cap, base × 2^attempt)); it spreads retries across the whole window and is a sound default.
- Equal jitter keeps half the delay fixed and randomises the rest, which guarantees a minimum wait between attempts.
- Decorrelated jitter grows from the previous wait: min(cap, random(base, previous × 3)).
- Without jitter, thousands of sends that failed together retry together and can knock a recovering service over again.
Operating sequence
How does backoff jitter work?
Compute the exponential ceiling
For retry n, take
min(cap, base × factor^n). With a 1 s base, factor 2 and a 30 s cap the ceilings run 1, 2, 4, 8, 16, 30, 30 s.Draw a random wait inside it
Full jitter: a uniform random value between 0 and the ceiling. Equal jitter: half the ceiling plus a random value up to the other half. Decorrelated: a random value between the base and three times the previous sleep, capped.
Respect the server's own hint
If a response says when to come back — an HTTP
Retry-Afterheader, or a known rule such as waiting at least 24 hours after a per-user marketing-limit error (131049) — use it as the floor and add jitter on top.Cap attempts and total time
Stop after a fixed number of attempts or a deadline that matches the message's value. An OTP is useless once its validity has passed, so it should never be retrying an hour later.
Give up cleanly
When retries run out, move the job to a dead-letter queue with its last error, rather than retrying forever or dropping it silently.
Specifications
Backoff jitter: key facts and limits
- Full jitter
- random(0, min(cap, base × 2^n))
- Equal jitter
- t/2 + random(0, t/2), where t = min(cap, base × 2^n)
- Decorrelated jitter
- min(cap, random(base, previous × 3))
- Typical settings
- Base 0.5–1 s, cap 30–60 s, 5–8 attempts (illustrative)
- Retry with jitter
- 130429, 131056, 131000, 131016, HTTP 5xx, timeouts
- Never retry
- Invalid parameters (132000), re-engagement (131047)
Implementation
Backoff jitter in code
type Jitter = "none" | "full" | "equal" | "decorrelated";const BASE_MS = 1_000;const CAP_MS = 30_000;/** Wait before retry number `attempt` (0-based). `prevMs` feeds decorrelated jitter. */export function backoffDelay(attempt: number, mode: Jitter, prevMs = BASE_MS): number {const ceiling = Math.min(CAP_MS, BASE_MS * 2 ** attempt);switch (mode) {case "none":return ceiling;case "full":return Math.random() * ceiling;case "equal":return ceiling / 2 + Math.random() * (ceiling / 2);case "decorrelated":return Math.min(CAP_MS, BASE_MS + Math.random() * (prevMs * 3 - BASE_MS));}}// Transient Graph errors worth retrying: throughput, pair rate, unknown, service, genericconst RETRYABLE = new Set([130429, 131056, 1, 2, 131000, 131016]);export async function sendWithRetry(payload: object, maxAttempts = 6) {const url = `https://graph.facebook.com/v26.0/${process.env.PHONE_NUMBER_ID}/messages`;let prev = BASE_MS;for (let attempt = 0; attempt < maxAttempts; attempt++) {const res = await fetch(url, {method: "POST",headers: {Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,"Content-Type": "application/json",},body: JSON.stringify(payload),});if (res.ok) return res.json();const code = (await res.json()).error?.code;if (res.status < 500 && !RETRYABLE.has(code)) throw new Error(`Not retryable: ${code}`);const hintMs = Number(res.headers.get("retry-after")) * 1000 || 0;prev = Math.max(hintMs, backoffDelay(attempt, "full", prev));await new Promise((resolve) => setTimeout(resolve, prev));}throw new Error("Retries exhausted: move the job to the dead-letter queue");}
All three jitter variants, and a send loop that retries only transient errors with full jitter and honours Retry-After.
Example: A 50,000-message Diwali broadcast from Ahmedabad
A sweets chain in Ahmedabad queues 50,000 marketing templates across 40 workers. A misconfigured rate limiter lets them burst past the number's 80 messages per second, and about 6,000 sends fail with 130429 within a few seconds. With plain exponential backoff every one of them retries exactly 1 s later, triggering a second wave of 130429s, then a third at 2 s and a fourth at 4 s. After the team fixes the limiter and switches to full jitter (1 s base, 30 s cap), the same 6,000 retries spread across each window, the token bucket in front of the sender absorbs them, and the broadcast finishes with no second wave.
Design intent
Why does backoff jitter matter?
Retries are meant to ride out a blip, but synchronised retries can turn a two-second blip into a ten-minute outage. For a WhatsApp sender the cost is concrete: throttled numbers that keep slamming into their throughput ceiling, a CRM that falls over each time it recovers, and broadcasts that finish late. Jitter is a two-line change that spreads the same retry volume evenly across the window, so the dependency sees a smooth slope instead of a wall.
Failure modes
Common mistakes with backoff jitter
Jittering the wrong errors
Jitter makes retries polite, not useful. A 132000 parameter mismatch or a 131047 re-engagement error fails on every attempt; send it to the dead-letter queue instead of retrying it, however randomly.
Seeding every worker identically
Workers seeded with a fixed value for reproducible tests, or forked with a copied generator state (some libraries do not reseed after a fork), draw the same "random" sequence and retry in lockstep anyway. Seed per process, or use the runtime's default random source in production.
Jitter without a cap
Uncapped exponential growth reaches waits of many minutes after a handful of failures, and full jitter can still draw near the top. Cap each delay and the total retry time per message.
Retrying inside and outside at once
An HTTP client with its own retries, wrapped in a queue that also retries, multiplies attempts — 3 × 5 becomes 15 calls per message. Retry in one layer and make the others fail fast.
Recommended practice
Best practices for backoff jitter
- Default to full jitter; reach for equal jitter only when a minimum wait matters more than spreading load.
- Log the drawn delay with each retry so you can see the spread during an incident.
- Pair jitter with a client-side token bucket algorithm so retries also respect the number's throughput.
- Test with many concurrent clients failing at once — a single-client test cannot show a herd.