Glossary · E

Exponential Backoff

A retry strategy that multiplies the wait time after each failed attempt, up to a maximum, so struggling services get room to recover.

By Obseria Editorial TeamUpdated

Definition

Exponential backoff is a retry strategy in which the wait time before each new attempt grows by a constant factor, usually doubling, up to a maximum delay. Combined with random jitter and a limit on attempts, it spreads retries over time so that clients do not overwhelm a service that is already failing or rate limiting.

What is exponential backoff?

Exponential backoff is the standard way to retry failed remote calls without making the failure worse. Instead of retrying immediately or at a fixed interval, a client waits a little after the first failure, twice as long after the second, four times as long after the third, and so on, until it either succeeds, reaches a maximum delay or gives up after a set number of attempts.

The idea is simple: if a dependency is overloaded, every immediate retry adds load at exactly the wrong moment. Growing delays reduce retry pressure quickly and give the dependency time to recover, while still masking short, transient errors such as a dropped connection or a brief leader election.

The exponential backoff formula

The basic formula for attempt n (starting at 0) is:

delay = min(cap, base * 2^n)

base is the initial delay, 2 is the multiplier (some libraries use 1.5 or make it configurable) and cap is the maximum delay. With an illustrative base of 100 ms and a cap of 10 s, the delays are 100 ms, 200 ms, 400 ms, 800 ms, 1.6 s, 3.2 s, 6.4 s and then 10 s for every further attempt. A maximum number of attempts, or an overall deadline, ends the sequence.

Why exponential backoff needs jitter

Pure exponential backoff has a flaw: if a thousand clients fail at the same moment, they all retry after exactly 100 ms, then after exactly 200 ms, and keep hitting the service in synchronized waves. Adding randomness, called jitter, breaks up those waves. Common variants, popularized by an AWS Architecture Blog article on backoff and jitter:

VariantFormulaBehavior
No jittermin(cap, base * 2^n)Synchronized retry waves
Full jitterrandom(0, min(cap, base * 2^n))Spreads retries widely, low total load
Equal jitterd/2 + random(0, d/2) with d = min(cap, base * 2^n)Guarantees at least half the delay
Decorrelated jittermin(cap, random(base, previous * 3))Each delay depends on the previous one

Full jitter is a good default for most clients. The important point is to use some jitter at all.

Example: retries in Go

base, maxDelay := 100*time.Millisecond, 10*time.Second
for attempt := 0; attempt < 6; attempt++ {
    err := callPaymentAPI(ctx)
    if err == nil || !isRetryable(err) {
        return err
    }
    d := base << attempt          // base * 2^attempt
    if d > maxDelay { d = maxDelay }
    time.Sleep(time.Duration(rand.Int63n(int64(d)))) // full jitter
}

Only retryable errors are retried, for example timeouts, HTTP 429, 502, 503 and 504 or gRPC UNAVAILABLE. A 400 Bad Request will fail the same way every time. If the server sends a Retry-After header with a 429 or 503 response, honor it instead of the computed delay; this is how backoff cooperates with server-side rate limiting.

Best practices

  • Retry only idempotent operations, or use idempotency keys, so a retry after a timeout does not charge a customer twice.
  • Bound everything: a cap on the delay, a maximum number of attempts and an overall deadline that fits the caller's own timeout.
  • Retry at one layer. If three layers each make three attempts, the bottom service can see up to 27 calls per user request. Retry budgets limit retries to a fraction of normal traffic.
  • Combine with a circuit breaker that stops calls entirely when a dependency is clearly down.
  • Use the built-in policies of your SDK or framework. AWS SDKs, gRPC retry policies (initialBackoff, maxBackoff, backoffMultiplier) and resilience libraries implement backoff with jitter correctly.
  • Hand off to a queue for work that can wait: after the last attempt, move the message to a dead letter queue instead of retrying forever.

Making retries observable

Retries hide problems as well as solve them. A request that succeeds on the fourth attempt looks healthy in an error counter but adds seconds of latency. Record the attempt number as an attribute, emit a counter of retries per dependency, and make each attempt visible as its own client span. A sudden rise in the retry rate is often the earliest sign that a downstream dependency is degrading. Also compare the number of attempts with the number of original requests: if that ratio climbs sharply, your clients are amplifying load on a service that is already in trouble.

How Obseria helps

With OpenTelemetry instrumentation, each retry attempt appears as a separate span in distributed tracing, so Obseria shows where time went and which dependency caused the retries. Retry counters and latency can be queried together in OQL or PromQL and used for alerts before users notice.

Frequently asked questions

The delay before attempt n is min(cap, base * 2^n), where base is the initial delay and cap the maximum. In practice a random jitter is applied to this value to avoid synchronized retries.

Exponential backoff makes the wait grow with every attempt. Jitter adds randomness to that wait so that many clients do not retry at exactly the same moment. They are normally used together.

Transient errors such as timeouts, connection resets, HTTP 429, 502, 503 and 504 or gRPC UNAVAILABLE. Client errors like 400 or 404 should not be retried, because they will fail again.

It depends on the caller's deadline, but a small number such as three to five attempts with a capped delay is common for synchronous requests. Background jobs can retry longer and then hand off to a dead letter queue.

Put these concepts into practice

Try Obseria free for 14 days, with OpenTelemetry and no proprietary agents.