GLOSSARY

What is Exponential Backoff?

Exponential backoff is a retry strategy in which the wait before each attempt is multiplied by a constant factor, so a client that keeps failing puts exponentially less pressure on the service it is failing against.

Free forever plan · No credit card required · Cancel anytime

Quick definition

Exponential backoff is a retry strategy in which the wait before each attempt is multiplied by a constant factor, so a client that keeps failing puts exponentially less pressure on the service it is failing against.

The formula, in one line: delay(n) = min(cap, base x multiplier^n)

What it means

Exponential backoff is what a well-behaved client does after a request fails: wait, try again, and if it fails again, wait longer. The waits grow geometrically, usually doubling, until they hit a cap or the client gives up.

The idea is older than the web. Ethernet's binary exponential backoff, part of the CSMA/CD standard, resolves collisions on a shared wire by having each station wait a random multiple of a doubling slot time. TCP does the same with its retransmission timeout, doubling the RTO on each loss as described in RFC 6298. HTTP clients inherited a pattern that already had two decades of evidence behind it.

The reason it works is worth stating precisely, because it is not "be polite". A failing service is usually failing because it is saturated. Every retry sent during that window is load added at the worst possible moment. Backoff makes the aggregate retry load decay geometrically, which gives the service headroom to recover. Constant-interval retries do the opposite: they hold the load flat and can keep a service down long after the original trigger has passed.

The arithmetic nobody does

Almost every article about backoff stops at the formula. The table below runs it: base 500 ms, multiplier 2, cap 60 seconds. The column that matters is the cumulative one, because that is the number your operations team actually cares about, and it is invisible in the formula.

Eight retries at those settings spend 123.5 seconds sleeping. That is your real deadline: if the operation has to complete within two minutes, eight retries is already too many, and no amount of tuning the multiplier fixes it. Ten retries takes 243.5 seconds and only adds two more chances, because everything past retry 8 is a flat 60-second poll.

The second piece of arithmetic is retry amplification. A uniform policy of three retries means four attempts per operation. During a total outage, when every attempt fails, the service receives four times its normal request volume from a client base that is doing nothing unusual. This is how a five-minute partial degradation becomes an hour-long outage, and it is the argument for a retry budget: cap retries as a fraction of successful traffic, typically ten percent, and drop the rest rather than sending them.

What to retry, and what never to retry

The status code decides. Retrying a 422 is a loop that cannot terminate successfully; not retrying a 502 throws away a request that would probably have worked the second time.

One case is worth more than a row in a table. The Pinlyx Data API returns 503 for three genuinely different conditions and expects you to branch on error.code rather than on the status:

  • upstream_timeout: a source was too slow. This is also what a catalogue query hitting its 15-second statement timeout returns, so it can reach you from any endpoint that reads the corpus, not only the live ones. Retrying with backoff is worthwhile, and narrowing the request (a smaller limit, a tighter filter, a less popular account) makes it far less likely to recur.
  • upstream_error: a source was unreachable. Back off further than you would for a timeout.
  • not_configured: the capability has no backing service in this deployment. Retrying will never help. A client that treats all 503s alike will retry this one until its budget is gone, every single time.

That is the general lesson: the status code is a category, the error code is the diagnosis. Any API worth integrating with gives you both.

Backoff is not the whole answer

Three things belong next to it.

Jitter. Pure exponential backoff synchronises clients. Everyone who failed at time T retries at T plus 1, then at T plus 3, then at T plus 7, in unison. The retry curve becomes a series of spikes, each as tall as the original burst. Randomising the delay flattens them, and it is one line of code. See jitter for the three standard strategies and their trade-offs.

A deadline, not just an attempt count. "Five retries" means something different for a 40 ms call than for one that times out after 30 seconds. Track total elapsed time and abandon on that, with the attempt count as a secondary guard.

A circuit breaker. Backoff still sends a request per attempt. When a dependency has been down for ten minutes, sending anything is waste. A breaker that opens after N consecutive failures, fails fast without touching the network, and half-opens periodically to probe, removes that waste entirely and gives the recovering service a much gentler ramp.

A fixed ladder can beat a computed curve

Not every retry schedule should be a formula. The Data API's webhook delivery pipeline retries a failed delivery up to 8 times on a published, fixed ladder: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours, and then the delivery is marked failed. It is never retried forever.

The step ratios there are 5, 6, 4, 3, 2 and 2, which is not exponential at all. That is deliberate. The thing a webhook receiver actually needs to survive is not a smooth mathematical curve, it is a set of human events: a deploy, a night, a weekend morning. A ladder whose total span is 44 hours 36 minutes covers a receiver that broke on Friday evening and was fixed on Sunday. A doubling curve capped at 60 seconds would have given up in the first two minutes.

The corollary matters for receivers: your de-duplication store has to outlive the sender's ladder. If the sender can re-deliver 44 hours after the first attempt and you keep processed event ids for 24 hours, the last retry is processed twice. Seven days is the safe number here.

Related concepts

  • Jitter: the randomness that stops backoff from synchronising clients.
  • Retry-After: the server's own instruction, which outranks your formula.
  • Rate limit: the most common reason a retry is refused.
  • Idempotency key: what makes retrying a write safe rather than merely slower.
  • Flood wait: Telegram's version of "wait this long", with its own backoff rules.

How Pinlyx handles it

Retries with backoff are built into both directions of the product. Outbound messages that hit a platform flood wait are requeued with an automatic backoff rather than failing the job. Webhook deliveries follow the published 8-attempt ladder above and expose every attempt, its status code, its error and its duration, so a failed delivery can be diagnosed rather than guessed at. And every Data API error carries a stable code so a client can tell "try again in a moment" apart from "this will never work".

Cheat sheet · the schedule, computed

base 500 ms, multiplier 2, cap 60 s.

Delay is what you sleep before that attempt. Cumulative is how long the operation has been failing by then, which is the number that decides whether your deadline survives.

AttemptFormulaDelayCumulative sleep
Retry 10.5 x 2^00.5 s0.5 s
Retry 20.5 x 2^11 s1.5 s
Retry 30.5 x 2^22 s3.5 s
Retry 40.5 x 2^34 s7.5 s
Retry 50.5 x 2^48 s15.5 s
Retry 60.5 x 2^516 s31.5 s
Retry 70.5 x 2^632 s63.5 s
Retry 8capped from 64 s60 s123.5 s
Retry 9capped60 s183.5 s
Retry 10capped60 s243.5 s

Compare with the fixed ladder used for webhook deliveries: 1 m, 5 m, 30 m, 2 h, 6 h, 12 h, 24 h, total span 44 h 36 m. Different job, different curve.

Retry or do not retry

The decision is the status code, then the error code.

ResponseRetry?Why
408 Request TimeoutYesThe request never completed. Nothing was applied.
429 Too Many RequestsYes, after Retry-AfterA pacing signal, not an error. Read the code first: a daily quota does not clear for hours.
500 Internal Server ErrorYes, cautiouslyMight be transient. Cap the attempts and be sure the operation is idempotent.
502 / 504YesA gateway or upstream failed. The origin may never have seen the request.
503 Service UnavailableDepends on the codeupstream_timeout is worth retrying narrower, upstream_error wants a longer wait, not_configured will never succeed.
400 / 422NoThe request is malformed. Repeating identical bytes produces an identical refusal.
401 / 403NoA missing key or a missing scope. Fix the credential; time changes nothing.
402 Payment RequiredNoThe plan cannot pay for the call. Retrying spends attempts, not budget.
404 Not FoundNoUnless you are polling for a resource you know is being created, in which case cap it hard.
Cheat sheet · a retry helper worth copying

Deadline, Retry-After, jitter, and a code-aware give-up.

const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
const NEVER_RETRY_CODES = new Set(["not_configured", "quota_exceeded",
                                   "payment_required", "forbidden_scope"]);

async function callWithBackoff(fetchOnce, {
  base = 500, factor = 2, cap = 60_000, maxRetries = 8, deadlineMs = 300_000,
} = {}) {
  const startedAt = Date.now();

  for (let attempt = 0; ; attempt++) {
    const res = await fetchOnce();
    if (res.ok) return res;

    const body = await res.clone().json().catch(() => null);
    const code = body?.error?.code;

    // Some failures are permanent no matter how long you wait.
    if (!RETRYABLE.has(res.status) || NEVER_RETRY_CODES.has(code)) return res;
    if (attempt >= maxRetries) return res;
    if (Date.now() - startedAt > deadlineMs) return res;

    // The server's own instruction outranks the formula.
    const header = res.headers.get("Retry-After");
    const serverWait = header ? parseRetryAfter(header) : 0;
    const computed = Math.min(cap, base * factor ** attempt);
    const floor = Math.max(computed, serverWait);

    // Full jitter: anywhere in [0, floor]. Never below the server's floor
    // when the server actually named one.
    const wait = serverWait > 0
      ? serverWait + Math.random() * Math.min(serverWait, 5_000)
      : Math.random() * floor;

    await new Promise((r) => setTimeout(r, wait));
  }
}
Watch out for

Backoff without the rest of it is still a retry storm.

A doubling delay with no jitter synchronises every client that failed at the same instant, so the load arrives as spikes instead of a flood. A doubling delay with no cap sleeps for days. A doubling delay with no deadline holds a request handler open until it times out somewhere else. And a doubling delay in front of a non-idempotent write produces duplicates on a widening schedule.

The complete recipe is five parts: exponential growth, a cap, jitter, a deadline, and a rule for which failures are worth repeating at all. Ship fewer than five and you have shipped the shape of a retry policy without the behaviour.

Exponential backoff: FAQ

The parameters, the limits, and the failures that should never be retried.

Base 500 ms, multiplier 2, cap 60 seconds, maximum 8 retries is a defensible default for a server-to-server API call, and it is the schedule computed on this page: about 2 minutes 3 seconds of total sleep across 8 retries. For a user-facing request, the cap should be your own response deadline instead, because a client that sleeps 60 seconds inside a request handler has turned a transient error into a timeout of its own.
No. Retry-After outranks your formula. If the server said 47 seconds and your curve says 2, waiting 2 seconds produces one guaranteed-refused request and one wasted unit of quota. The rule is delay = max(computed_backoff, retry_after), then add jitter on top. Your formula is what you use when the server did not tell you, which for 500 and 502 responses is most of the time.
Only if the operation is idempotent, either because the server accepts an Idempotency-Key or because the write upserts on a natural key. RFC 9110 section 9.2.2 makes GET, HEAD, PUT, DELETE, OPTIONS and TRACE idempotent by definition; POST is excluded precisely because a repeat can create a second resource. Backoff without idempotence does not make retries safe, it just makes duplicates arrive more slowly.
They solve different halves of the same problem. Backoff protects the service from one client; a circuit breaker protects one client from the service. After a threshold of consecutive failures the breaker opens and fails calls immediately without touching the network, then after a cooldown it half-opens and lets a single probe through. Backoff decides how long to wait between attempts; the breaker decides whether to attempt at all. Production clients want both.
Because an uncapped doubling grows absurdly fast. Starting at 500 ms, retry 20 would sleep for about 6 days. A cap converts the tail of the curve into a steady polling interval, which is what you actually want during a long outage: keep checking, but at a rate the recovering service can absorb when it comes back and every client reconnects at once.
Ready to ship

Fail slower. Recover faster.

Stable error codes on every response, a published retry ladder on every webhook delivery, and a full attempt log when something does not arrive.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.