WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Engineering Practice

Exponential Backoff and Retry Logic: Building Resilient Network Calls

Retrying a failed call sounds like the obvious right move. Done naively, it's also one of the most common ways engineers turn a brief blip into a full outage.

Published July 6, 2026

A network call can fail for reasons that have nothing to do with whether trying again would help: a dropped packet, a momentary DNS hiccup, a downstream service that's briefly overloaded. Retrying makes sense for exactly these transient cases. The trouble starts with how most first attempts at retry logic are written — retry immediately, a fixed number of times, with no delay between attempts.

Why an immediate retry can make things worse

If a downstream service is failing because it's overloaded, a client retrying instantly just adds more load to an already-struggling service, at the exact moment it can least handle it. Multiply that by every client hitting the same failing service simultaneously, and you get a retry storm: the retries themselves become a bigger load spike than the original traffic, which can turn a brief degradation into a full outage that takes far longer to recover from, because the service never gets a moment of reduced load to catch up.

Exponential backoff: wait longer each time

Instead of retrying immediately, each successive retry waits longer than the last, typically doubling: 1 second, then 2, then 4, then 8, up to some maximum. This gives a struggling downstream service progressively more breathing room with each retry cycle, rather than constant pressure at the same rate that caused the failure in the first place.

function backoffDelay(attempt, base = 1000, max = 30000) {
  const delay = Math.min(base * 2 ** attempt, max);
  return delay;
}
// attempt 0: 1000ms, attempt 1: 2000ms, attempt 2: 4000ms, ...

Jitter: why the same formula on every client is still a problem

If a thousand clients all fail at the same moment (a common case, since they're often all hitting the same overloaded service) and all use identical exponential backoff, they retry in lockstep — every client hits the service again at exactly 1 second, then again at exactly 2 seconds, recreating the same synchronized load spike on every retry cycle, just spaced further apart. Jitter fixes this by adding randomness to the delay so retries spread out over time instead of arriving in a synchronized burst:

function backoffWithJitter(attempt, base = 1000, max = 30000) {
  const capped = Math.min(base * 2 ** attempt, max);
  return Math.random() * capped; // "full jitter"
}

This specific approach, picking a random delay between zero and the exponential cap rather than the cap itself, is often called "full jitter," and it spreads retry traffic out more effectively than adding a small random offset to an otherwise fixed exponential delay.

Not every failure should be retried

A 400 Bad Request means the request itself was malformed — retrying the identical request will fail identically, every time, forever. Only retry errors that are plausibly transient: connection timeouts, 502/503/504 responses, and rate-limit responses (ideally honoring a Retry-After header if the server sends one rather than guessing). Blindly retrying every failure, including ones that can never succeed on retry, wastes calls and delays surfacing a real, permanent problem to whoever needs to see it.

Retries and idempotency are a package deal

A retry means the same request might be processed twice: the first attempt could have actually succeeded on the server, with only the response getting lost on the way back to the client. If the operation isn't idempotent — "charge this card $50" rather than "set this account's balance to $200" — a retry can double-charge a customer or double-send an email. Safe retry logic for non-idempotent operations needs an idempotency key: a unique identifier attached to the logical operation that the server can check against requests it's already processed, so a retried request with the same key gets the original result handed back rather than being executed a second time.

Bounding it: a retry budget, not infinite attempts

Backoff and jitter control the pace of retries; they don't decide when to stop entirely. A hard cap on retry count, or a total time budget for how long a caller is willing to keep retrying before giving up and surfacing the failure, is what prevents "keep retrying with backoff" from silently turning into "hang for two minutes before the user ever sees an error." Pair backoff with a circuit breaker for calls to a dependency that's failing consistently, so the system stops sending traffic at all once it's clear the dependency is down rather than continuing polite, spaced-out retries against something that has no chance of succeeding.