01What is exponential backoff?
Exponential backoff is a retry schedule in which the wait before each new attempt grows geometrically. After the first failure a client waits a base delay; after the next it waits that delay times a multiplier, then times the multiplier again, and so on, until a maximum delay (the cap) is reached. With a 1-second base, a factor of 2 and a 30-second cap, retries wait 1, 2, 4, 8, 16, 30 and 30 seconds. The formula is delay = min(cap, base x factor^n), where n counts retries from zero.
The growing gaps matter because the most common reasons for a failed call (a server under load, a rate limit, a brief network fault) get worse when everyone retries immediately. Fixed or instant retries keep the pressure on; exponential gaps back off quickly, giving the dependency room to recover while still retrying fast for a one-off blip. Most clients pair it with a retry limit or an overall deadline, after which the job goes to a dead-letter queue or the error is reported.
On the WhatsApp Cloud API the classic triggers are Error 130429 (Rate Limit Hit), raised when a number exceeds its throughput (up to 80 messages per second by default), error 131056, the pair rate limit on messages to one recipient, HTTP 5xx responses and timeouts. Meta uses the same idea in the other direction: under its webhook retry policy, a failing webhook endpoint gets deliveries retried with decreasing frequency for up to seven days.
Exponential backoff is only the schedule. Backoff jitter randomises each delay so thousands of clients that failed together do not retry together (a thundering herd), and the retry policy decides which failures are worth retrying at all. Proactive rate limiting paces sends so the backoff rarely fires, and a circuit breaker stops retrying altogether for a while when a dependency is clearly down.
Key takeaways
- Exponential backoff waits base x factor^n before retry n, so with a 1 s base and a factor of 2 the delays grow 1 s, 2 s, 4 s, 8 s.
- A cap stops the waits growing forever; a retry limit or deadline decides when to give up and dead-letter the job.
- Use it for transient failures only: timeouts, HTTP 5xx, throughput error 130429 and pair rate limit 131056.
- Backoff sets the schedule; jitter randomises it and the retry policy decides whether to retry at all.
- Meta applies its own decreasing-frequency retries to your webhook endpoint for up to 7 days.
02 · How it works, step by step
How does exponential backoff work?
- Step 1
Classify the failure first
Only transient errors enter the backoff loop: timeouts, connection resets, HTTP 5xx,
130429and131056. Permanent ones, such as an invalid recipient, a paused template or a missing permission, fail fast instead. - Step 2
Compute the next delay
For retry n (starting at 0), wait
base x factor^n. A factor of 2 doubles each wait; a smaller factor such as 1.5 grows more gently for latency-sensitive work like OTPs. - Step 3
Cap the delay
Apply
min(cap, delay)so the tenth retry does not wait seventeen minutes. Caps of 30 to 60 seconds suit interactive sends; background jobs can use longer. - Step 4
Randomise with jitter
Replace the exact delay with a random value up to it (full jitter) so clients that failed at the same moment spread their retries out instead of returning in lockstep.
- Step 5
Stop on a limit or deadline
End after a fixed number of retries or when the message would no longer be useful, such as an OTP older than its expiry. Then dead-letter the job, record the error code and
fbtrace_id, and alert if the rate climbs.
03Exponential backoff in code
const RETRYABLE_HTTP = new Set([429, 500, 502, 503, 504]);const RETRYABLE_META = new Set([130429, 131056]); // throughput, pair rate limitexport function backoffDelay(retry, { baseMs = 1000, factor = 2, capMs = 30_000 } = {}) {return Math.min(capMs, baseMs * factor ** retry); // 1s, 2s, 4s, 8s, 16s, 30s, 30s...}export async function sendWithBackoff(payload, { maxRetries = 6 } = {}) {for (let retry = 0; ; retry++) {let res;let err = {};try {res = await fetch(`https://graph.facebook.com/v26.0/${process.env.PHONE_NUMBER_ID}/messages`, {method: "POST",headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}`, "Content-Type": "application/json" },body: JSON.stringify(payload),signal: AbortSignal.timeout(10_000),});if (res.ok) return await res.json();err = (await res.json().catch(() => ({}))).error ?? {};} catch (e) {err = { message: e.message }; // timeout or network error: transient (but may have been delivered)}const transient = !res || RETRYABLE_HTTP.has(res.status) || RETRYABLE_META.has(err.code);if (!transient || retry >= maxRetries) {throw Object.assign(new Error(err.message ?? "send failed"), { code: err.code, fbtrace_id: err.fbtrace_id });}const delay = backoffDelay(retry);await new Promise((r) => setTimeout(r, Math.random() * delay)); // full jitter on top}}
Retry only transient failures, grow the wait geometrically, cap it, randomise it, and give up after a fixed number of retries.
04Exponential backoff: key facts and limits
- Formula
- delay = min(cap, base x factor^n), n = 0 for the first retry
- Example schedule
- 1, 2, 4, 8, 16, 30 s: 61 s of waiting over 6 retries
- Throughput error
- 130429 (default up to 80 mps per number)
- Pair rate limit error
- 131056 (too fast to one recipient)
- Never back off and retry
- Invalid recipient, paused template, missing permission
- Meta's webhook retries
- Decreasing frequency for up to 7 days
05Example: A festive broadcast meets the throughput cap
A Surat saree wholesaler queues 40,000 marketing templates at 10 am. Twelve workers send as fast as they can, the number exceeds its throughput, and a share of requests return 130429. Before the fix, the workers retried instantly and the error rate stayed above 30% for twenty minutes. With backoff at a 1-second base, a factor of 2, a 30-second cap and full jitter, plus a shared pacing limit just under the number's throughput, the errors fall to a trickle within a minute and the campaign completes with no messages dead-lettered.
06 · Common mistakes, and the fix
Common mistakes with exponential backoff
No cap and no limit
Without a cap, retry 12 waits over an hour; without a limit, a permanently broken job retries forever and hides the failure. Set both, and send exhausted jobs to a dead-letter queue with the last error attached.
Backing off on permanent errors
Retrying an invalid phone number or a paused template just delays the inevitable and wastes throughput. Check the error code and fail fast on anything a retry cannot fix.
Retrying a send that may already have worked
A timeout does not mean the message was not delivered. Before retrying a timed-out send that matters, check whether a wamid or status webhook arrived for it, or accept the small duplicate risk deliberately.
Backing off in lockstep
Thousands of jobs that failed in the same second and use the same schedule all retry in the same second too. Add jitter, and pace sends with a shared limiter so the cap is rarely hit at all.
Ignoring the message's useful life
An OTP retried after two minutes of backoff may arrive after the code expired. Bound retries by a deadline for time-sensitive messages, not just by an attempt count.
07 · Tips from the bench
Best practices for exponential backoff
- Put the backoff in one shared send function, so every caller gets the same behaviour.
- Log each retry with attempt number, delay, error code and
fbtrace_id; the pattern tells you whether it is a cap or an outage. - Use a smaller base and factor for OTPs and a larger cap for bulk campaigns.
- If a dependency returns a
Retry-Afterheader, treat it as the minimum wait.
08Why does exponential backoff matter?
Retries without backoff turn a small incident into a big one: a broadcast that hits the throughput cap and retries instantly multiplies its own traffic, collects more 130429 errors, and can drag other sends on the same number down with it. A well-chosen schedule recovers by itself from blips, sheds load during real trouble and keeps your workers from spinning. It is also the cheapest reliability fix there is: a dozen lines of code in the one function every send goes through.