jonnifyredis

R4.04.1 · dive 1

Exponential backoff

A retry delay grows with the attempt: delay = base × 2^(attempt - 1). Its owner is EchoMQ.Backoff.calculate/4 — in Elixir, not the Lua.

A fixed delay treats every failure the same. An exponential delay doubles each attempt, so the first retry is quick and a repeatedly-failing dependency gets progressively more room. The whole behaviour rests on one expression: base × 2^(attempt - 1). Move the two sliders on the right to set a base and an attempt and read the delay it produces, against the linear and fixed delays for the same attempt.

The delay, computed · exponential vs linear vs fixed

1000
3
delay (ms) for the chosen attempt — exponential — linear — fixed

The formula

Exponential backoff scales the wait with the attempt number:

# delay = base × 2^(attempt - 1)
attempt 1 → base × 2^0 = base
attempt 2 → base × 2^1 = 2 × base
attempt 3 → base × 2^2 = 4 × base
attempt 4 → base × 2^3 = 8 × base

The exponent is attempt - 1, so the first attempt waits exactly the base. With a one-second base the retries land at 1s, 2s, 4s, 8s, 16s out from each failure. The delay doubles each step — a geometric series, not a linear one. Where a fixed 1s delay re-fires five times in five seconds, the exponential schedule spreads the same five attempts across thirty-one seconds of cumulative wall-clock, giving a struggling dependency exponentially more time to recover between hits.

Exponential backoff doubles the delay each attempt: attempt 1 waits the base, and each later attempt doubles the last, spreading the retries over a geometric, not linear, span.

Where the math lives — EchoMQ.Backoff, not the Lua

The exponential formula is Elixir. EchoMQ.Backoff.calculate/4 is the owner; its :exponential clause computes the delay and applies jitter.

# EchoMQ.Backoff.calculate/4 — the :exponential clause (real)
def calculate(:exponential, attempt, base_delay, opts) do
  jitter = Keyword.get(opts, :jitter, 0)
  delay  = trunc(:math.pow(2, attempt - 1) * base_delay)
  apply_jitter(delay, jitter)
end

:math.pow(2, attempt - 1) is the doubling; base_delay scales it; trunc makes it an integer millisecond. The doc example is exact: calculate(:exponential, 3, 1000) returns 4000, and calculate(:exponential, 3, 1000, jitter: 0.2) returns about 4000 with the spread. The :fixed strategy is the contrast — it returns apply_jitter(base_delay, jitter), the same base every attempt, no doubling.

This is the binding distinction. The retry reschedule script (retryJob-11.lua) does not compute 2^(n-1). It re-adds the job to the delayed set at a fire-time it is given. The delay value is computed first, in EchoMQ.Backoff; the Lua only reschedules at that delay. The formula is the strategy module's; the reschedule is the script's.

The exponential math lives in EchoMQ.Backoff.calculate/4 (trunc(:math.pow(2, attempt - 1) × base_delay)); the Lua reschedules at the delay it is handed and never raises 2 to a power.

The base is the whole tuning knob

The base delay sets the floor and, with it, the whole curve. A 100 ms base gives 100ms, 200ms, 400ms — tight retries for a flaky-but-fast dependency. A 5000 ms base gives 5s, 10s, 20s — patient retries for a service that takes minutes to recover. The shape is fixed (doubling); the base slides the whole series up or down. EchoMQ's default base is 1_000 ms (%{type: :exponential, delay: 1_000, jitter: 0.2}), so the default schedule is 1s, 2s, 4s, 8s out, jittered. The interactive below sweeps the base across five attempts and reads the cumulative wall-clock the curve spends.

The curve · five attempts, base swept

1000
exponential delay per attempt (ms) —

The doubling shape is fixed; the base slides the whole series, so it is the single knob that sets how tight or how patient the retry schedule is.

In EchoMQ — the formula, in real code

The exponential delay is real code in echo/apps/echomq. EchoMQ.Backoff.calculate(:exponential, attempt, base_delay, opts) computes trunc(:math.pow(2, attempt - 1) × base_delay) then jitters it. The two built-in strategies are :exponential and :fixed; a custom strategy can be registered with EchoMQ.Backoff.register/2, but the doubling is the exponential clause. The delay it returns is the value passed to the reschedule — retryJob-11.lua re-adds the job to the delayed set at that fire-time. The formula and the reschedule are two functions, in two languages, with one direction of data: Elixir computes the delay, Lua reschedules at it.

# the exponential delay, attempt by attempt — base_delay = 1000 ms (real values)
calculate(:exponential, 1, 1000)  # => 1000   (1000 × 2^0)
calculate(:exponential, 2, 1000)  # => 2000   (1000 × 2^1)
calculate(:exponential, 3, 1000)  # => 4000   (1000 × 2^2)
the pattern

A retry delay grows with the attempt: delay = base × 2^(attempt - 1), doubling each step.

in EchoMQ

EchoMQ.Backoff.calculate(:exponential, attempt, base_delay, opts) computes trunc(:math.pow(2, attempt - 1) × base_delay) then jitters it; retryJob-11.lua reschedules at that value. The formula is Elixir; the reschedule is Lua.

Exponential backoff doubles the delay each attempt, and the doubling lives in EchoMQ.Backoff — the Lua reschedules at the delay it is handed, it never computes the power.

Where this is heading · EchoMQ 2.0 The formula is computed in Elixir and the delay is written to emq:{queue}:delayed today. EchoMQ 2.0 renames that key to emq:{queue}:delayed and bumps meta.version to echomq:2.0.0, but EchoMQ.Backoff.calculate/4 is untouched by the break — the doubling is arithmetic over the attempt number, independent of the keyspace it eventually writes to. The exponential curve this dive teaches is the same before and after the rename.
Door · not depth The full lifecycle subsystem — how the backoff delay feeds the reschedule, the attempts cap, and the worker pool that runs them — is the subject of the dedicated EchoMQ course. The retry and checkpoint lifecycle is E6 · Lifecycle controls. This dive teaches the formula; that course teaches the lifecycle that uses it.

References

The write that re-schedules a failed job at the exponential fire-time, the sorted set behind it, and the queue protocol EchoMQ ports.

Sources

Related in this course