Backoff policy
A backoff policy is a function from “how many times has this failed?” to “how long should I wait before trying again?” Decoupled from any specific retry or supervisor, so the calculation is trivially testable and reusable.
interface BackoffPolicy { delayFor(restartCount: number): number;}restartCount is 0-based — the first retry passes 0, the
second 1, and so on. Returns the delay in milliseconds.
Two built-ins, both producing values you can plug into a
BackoffSupervisor or
use standalone.
Exponential backoff
Section titled “Exponential backoff”import { exponentialBackoff } from 'actor-ts';
const policy = exponentialBackoff({ minMs: 200, maxMs: 10_000, randomFactor: 0.2,});
policy.delayFor(0); // ~200ms (±20% jitter)policy.delayFor(1); // ~400mspolicy.delayFor(2); // ~800mspolicy.delayFor(3); // ~1600mspolicy.delayFor(4); // ~3200mspolicy.delayFor(5); // ~6400mspolicy.delayFor(6); // ~10_000 (clamped)policy.delayFor(30); // ~10_000 (still clamped)The formula: min × 2^n clamped to max, multiplied by
1 + random(-randomFactor, +randomFactor).
Three knobs:
| Field | Meaning |
|---|---|
minMs | Floor for the delay. The first retry waits at least this long. |
maxMs | Ceiling for the delay. Once min × 2^n exceeds this, the policy returns maxMs (jittered). |
randomFactor (default 0.2) | Jitter fraction in [0, 1]. The actual delay is base * (1 + sign * randomFactor) where sign is uniform in [-1, 1]. |
Why exponential? A failing upstream usually needs time to
recover. Doubling the wait gives it more time without infinite
delays. And clamping at max prevents pathologically long waits
after many failures.
Why jitter? When N clients all retry at the same delay, they synchronize their next attempt — the next retry is a thundering herd, increasing the chance the upstream stays broken. Adding ±20 % noise scatters retries in time so the herd disperses.
Linear backoff
Section titled “Linear backoff”import { linearBackoff } from 'actor-ts';
const policy = linearBackoff({ minMs: 500, maxMs: 5_000, stepMs: 500,});
policy.delayFor(0); // ~500mspolicy.delayFor(1); // ~1000mspolicy.delayFor(2); // ~1500mspolicy.delayFor(3); // ~2000mspolicy.delayFor(8); // ~4500mspolicy.delayFor(9); // ~5000ms (clamped)policy.delayFor(20); // ~5000ms (still clamped)The formula: min + step × n clamped to max, with the same
jitter contract.
When linear? Niche — when you specifically want bounded growth rather than exponential. Examples:
- A polling cadence that should level off quickly (poll every 500 ms, 1 s, 1.5 s, … capped at 5 s).
- A producer rate-limiting itself when receiving backpressure — the rate of growth in the wait time matches the steady-state-acceptable load on the consumer.
Exponential is usually the better choice for recovery (give the upstream time to heal). Linear is better for steady-state adaptation.
Determinism in tests
Section titled “Determinism in tests”Both policies accept an optional random: () => number override:
let seed = 0;const deterministicRandom = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
const policy = exponentialBackoff({ minMs: 100, maxMs: 10_000, randomFactor: 0.2, random: deterministicRandom,});
// Now `policy.delayFor(n)` is reproducible.A linear congruential generator is good enough for tests; for
production randomness, the default Math.random is fine.
Standalone usage
Section titled “Standalone usage”The policy doesn’t have to feed into a supervisor. Anywhere you’d
write await new Promise(r => setTimeout(r, computeDelay(n))),
plug a policy in:
import { exponentialBackoff } from 'actor-ts';
const policy = exponentialBackoff({ minMs: 200, maxMs: 5_000 });
let attempt = 0;while (true) { try { const result = await someRiskyCall(); return result; } catch (err) { const delay = policy.delayFor(attempt++); await new Promise(r => setTimeout(r, delay)); if (attempt >= 5) throw err; }}For the more common case of “retry N times with backoff,” see
Retry, which has its own delay
options (without going through BackoffPolicy). The standalone
policy is most useful when you’re integrating with an existing
retry/scheduler abstraction and just need the delay-computation
piece.
Custom policies
Section titled “Custom policies”Implement the BackoffPolicy interface:
import type { BackoffPolicy } from 'actor-ts';
// Fibonacci backoff — for fun.function fibBackoff(opts: { maxMs: number }): BackoffPolicy { return { delayFor(n: number): number { let a = 100, b = 100; for (let i = 0; i < n; i++) { [a, b] = [b, a + b]; } return Math.min(a, opts.maxMs); }, };}Pass it into BackoffSupervisor via the policy option and you’ve
overridden the default exponential growth.
Where to next
Section titled “Where to next”- Backoff supervisor —
the actor-restart wrapper that consumes a
BackoffPolicy. - Retry — per-call retry with
its own delay options; doesn’t go through
BackoffPolicybut the underlying math is similar. - Circuit breaker — for when “wait longer between attempts” should escalate to “stop attempting entirely for a while.”
The exponentialBackoff
and linearBackoff API
references cover the full options.