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
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
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)
A retry delay grows with the attempt: delay = base × 2^(attempt - 1), doubling
each step.
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.
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.
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
- Redis — ZADD — the write that re-schedules the failed job at the computed backoff fire-time.
- Redis — Sorted sets — the timer-wheel the exponential delay writes into.
- BullMQ — the queue protocol — the backoff strategy EchoMQ ports, where the Lua scripts are the protocol.
Related in this course
- R4.04 · Backoff & retry — the module hub.
- R4.04.2 · Jitter & the thundering herd — the next dive: spreading a synchronized batch.
- R4.04.3 · Reusing the delayed ZSET — the reschedule the delay is handed to.
- R4.01 · The delayed queue — the
:delayedmachinery the retry reuses. - R4 · Time, Delay & Priority — the chapter.
- E6 · Lifecycle controls — the dedicated EchoMQ course: the retry lifecycle in depth.