Liking cljdoc? Tell your friends :D

d-core.libs.backoff

C1 — Backoff policies as pure functions.

Every policy returns a plain fn of attempt → delay in milliseconds, where attempt is the 1-based failure count ((retry-fn 1) is the delay after the first failure). This matches the contract d-core.libs.retriable already documents.

Units

Delays are always long milliseconds. This is the settled decision for this pillar: it matches d-core.libs.retriable (Thread/sleep), and it maps onto both existing ecosystem implementations with no conversion, which is what makes their migration provably behaviour-preserving (see backoff-test/matches-async-proxy-compute-backoff-ms and backoff-test/matches-http-calc-backoff-ms). java.time.Duration remains the vocabulary of Pillar A at the boundary; this policy layer stays in ms.

Jitter

:jitter accepts either a keyword or a number:

ValueDistribution
:noneno jitter — fully deterministic
:fulluniform [0, d) — AWS "full jitter"
:equaluniform [d/2, d) — AWS "equal jitter"
<number> kuniform [d-(k·d), d+(k·d)] — symmetric/proportional

The numeric form exists because it is exactly what d-core-http/.../client.clj already does with :jitter 0.2, so that call site can migrate without changing its observed delay distribution.

:rand-fn is an injection point (a 0-arg fn returning [0.0, 1.0), default rand). It is mandatory whenever jitter is active — a jittered policy without it would be untestable, and silently defaulting would make the dependency invisible at the call site.

Cap semantics

:max-ms caps the delay before jitter is applied. Consequently:

  • with :none, :full or :equal, the observed delay never exceeds the cap (:full is in fact strictly below it);
  • with numeric jitter k, the observed delay is bounded by max-ms · (1 + k), i.e. the cap bounds the jitter centre, not the absolute ceiling. This is deliberate: it is the behaviour of the existing d-core-http implementation, and changing it would change the retry envelope of a live client.

Purity

exponential-backoff, linear-backoff and constant-backoff are pure: the returned fn depends only on its attempt argument.

decorrelated-jitter is inherently stateful (its next delay depends on the previous draw, not on attempt), so it is deliberately not an attempt -> ms fn. It is exposed as a pure state transducer — see its docstring — rather than bending the common contract and hiding an atom.

C1 — Backoff policies as pure functions.

Every policy returns a plain fn of `attempt` → delay in milliseconds, where
`attempt` is the **1-based failure count** (`(retry-fn 1)` is the delay after
the first failure). This matches the contract `d-core.libs.retriable`
already documents.

## Units

Delays are **always `long` milliseconds**. This is the settled decision for
this pillar: it matches `d-core.libs.retriable` (`Thread/sleep`), and it maps
onto both existing ecosystem implementations with no conversion, which is what
makes their migration provably behaviour-preserving (see
`backoff-test/matches-async-proxy-compute-backoff-ms` and
`backoff-test/matches-http-calc-backoff-ms`). `java.time.Duration` remains the
vocabulary of Pillar A at the *boundary*; this policy layer stays in ms.

## Jitter

`:jitter` accepts either a keyword or a number:

| Value        | Distribution                                        |
| :----------- | :-------------------------------------------------- |
| `:none`      | no jitter — fully deterministic                     |
| `:full`      | uniform `[0, d)` — AWS "full jitter"                |
| `:equal`     | uniform `[d/2, d)` — AWS "equal jitter"             |
| `<number> k` | uniform `[d-(k·d), d+(k·d)]` — symmetric/proportional |

The numeric form exists because it is exactly what
`d-core-http/.../client.clj` already does with `:jitter 0.2`, so that call
site can migrate without changing its observed delay distribution.

`:rand-fn` is an injection point (a 0-arg fn returning `[0.0, 1.0)`, default
`rand`). It is **mandatory whenever jitter is active** — a jittered policy
without it would be untestable, and silently defaulting would make the
dependency invisible at the call site.

## Cap semantics

`:max-ms` caps the delay *before* jitter is applied. Consequently:

- with `:none`, `:full` or `:equal`, the observed delay never exceeds the cap
  (`:full` is in fact strictly below it);
- with numeric jitter `k`, the observed delay is bounded by
  `max-ms · (1 + k)`, i.e. the cap bounds the jitter *centre*, not the
  absolute ceiling. This is deliberate: it is the behaviour of the existing
  `d-core-http` implementation, and changing it would change the retry
  envelope of a live client.

## Purity

`exponential-backoff`, `linear-backoff` and `constant-backoff` are pure: the
returned fn depends only on its `attempt` argument.

`decorrelated-jitter` is *inherently stateful* (its next delay depends on the
previous draw, not on `attempt`), so it is deliberately **not** an
`attempt -> ms` fn. It is exposed as a pure state transducer — see its
docstring — rather than bending the common contract and hiding an atom.
raw docstring

constant-backoffclj

(constant-backoff)
(constant-backoff opts)

Returns a pure fn (attempt) -> delay-ms that always yields the same delay.

Options: :ms (default 500), plus the :jitter / :rand-fn pair documented on exponential-backoff.

Returns a pure fn `(attempt) -> delay-ms` that always yields the same delay.

Options: `:ms` (default `500`), plus the `:jitter` / `:rand-fn` pair
documented on `exponential-backoff`.
raw docstring

decorrelated-jitterclj

(decorrelated-jitter attempt state)
(decorrelated-jitter _attempt state opts)

AWS "decorrelated jitter" as a pure state transducer.

The AWS algorithm is a recurrence, not a closed form:

d(1) = base-ms
d(n) = min(max-ms, rand(base-ms, d(n-1) · multiplier))

d(n) depends on the previous draw, not on n, so it cannot satisfy this namespace's (attempt) -> delay-ms contract. Rather than hide an atom behind that contract, the recurrence is exposed honestly:

(decorrelated-jitter attempt state opts) ;=> {:delay-ms n :next-state s}

Thread :next-state into the following call. The function is pure — the same (attempt, state, opts) always yields the same result — so a retry loop keeps its own state locally and no two loops can couple. Because state also counts its own draws, restarting from initial-state on every loop is safe, and the first draw is always exactly base-ms.

(let [opts {:base-ms 100 :rand-fn (constantly 0.9)}
      s0   (initial-state opts)
      r1   (decorrelated-jitter 1 s0 opts)              ;=> base-ms
      r2   (decorrelated-jitter 2 (:next-state r1) opts)]
  [(:delay-ms r1) (:delay-ms r2)]) ;=> [100 280]

Options: :base-ms (default 100), :multiplier (default 3.0), :max-ms (default 30_000), :rand-fn (required).

Each draw is monotonic in rand-fn's value, so a constant :rand-fn yields the deterministic traces asserted in backoff-test.

AWS "decorrelated jitter" as a **pure state transducer**.

The AWS algorithm is a recurrence, not a closed form:

```
d(1) = base-ms
d(n) = min(max-ms, rand(base-ms, d(n-1) · multiplier))
```

`d(n)` depends on the previous *draw*, not on `n`, so it cannot satisfy this
namespace's `(attempt) -> delay-ms` contract. Rather than hide an atom behind
that contract, the recurrence is exposed honestly:

```clojure
(decorrelated-jitter attempt state opts) ;=> {:delay-ms n :next-state s}
```

Thread `:next-state` into the following call. The function is pure — the same
`(attempt, state, opts)` always yields the same result — so a retry loop keeps
its own state locally and no two loops can couple. Because `state` also counts
its own draws, restarting from `initial-state` on every loop is safe, and the
first draw is always exactly `base-ms`.

```clojure
(let [opts {:base-ms 100 :rand-fn (constantly 0.9)}
      s0   (initial-state opts)
      r1   (decorrelated-jitter 1 s0 opts)              ;=> base-ms
      r2   (decorrelated-jitter 2 (:next-state r1) opts)]
  [(:delay-ms r1) (:delay-ms r2)]) ;=> [100 280]
```

Options: `:base-ms` (default `100`), `:multiplier` (default `3.0`),
`:max-ms` (default `30_000`), `:rand-fn` (**required**).

Each draw is monotonic in `rand-fn`'s value, so a constant `:rand-fn` yields
the deterministic traces asserted in `backoff-test`.
raw docstring

exponential-backoffclj

(exponential-backoff)
(exponential-backoff opts)

Returns a pure fn (attempt) -> delay-ms implementing capped exponential backoff with configurable jitter.

attempt is the 1-based failure count; the first delay therefore equals :base-ms (exponent attempt - 1).

Options:

  • :base-ms first delay, default 100
  • :multiplier growth factor per failure, default 2.0
  • :max-ms cap applied before jitter, default 30_000
  • :jitter :none (default) | :full | :equal | number
  • :rand-fn 0-arg random source; required when :jitter != :none
(def f (exponential-backoff {:base-ms 100 :multiplier 2.0 :max-ms 1000}))
(map f [1 2 3 5]) ;=> (100 200 400 1000)

Deterministic test example:

(def f (exponential-backoff {:base-ms 100 :jitter :full
                             :rand-fn (constantly 0.5)}))
(f 3) ;=> 200
Returns a pure fn `(attempt) -> delay-ms` implementing capped exponential
backoff with configurable jitter.

`attempt` is the 1-based failure count; the first delay therefore equals
`:base-ms` (exponent `attempt - 1`).

Options:
- `:base-ms`       first delay, default `100`
- `:multiplier`    growth factor per failure, default `2.0`
- `:max-ms`        cap applied before jitter, default `30_000`
- `:jitter`        `:none` (default) | `:full` | `:equal` | number
- `:rand-fn`       0-arg random source; required when `:jitter` != `:none`

```clojure
(def f (exponential-backoff {:base-ms 100 :multiplier 2.0 :max-ms 1000}))
(map f [1 2 3 5]) ;=> (100 200 400 1000)
```

Deterministic test example:

```clojure
(def f (exponential-backoff {:base-ms 100 :jitter :full
                             :rand-fn (constantly 0.5)}))
(f 3) ;=> 200
```
raw docstring

initial-stateclj

(initial-state)
(initial-state opts)

Initial state for decorrelated-jitter. :base-ms defaults to 100.

Initial state for `decorrelated-jitter`. `:base-ms` defaults to `100`.
raw docstring

linear-backoffclj

(linear-backoff)
(linear-backoff opts)

Returns a pure fn (attempt) -> delay-ms implementing capped linear backoff: base-ms + increment-ms · (attempt - 1).

Options: :base-ms (default 250), :increment-ms (default 250), :max-ms (default 5_000), plus the :jitter / :rand-fn pair documented on exponential-backoff.

Returns a pure fn `(attempt) -> delay-ms` implementing capped linear backoff:
`base-ms + increment-ms · (attempt - 1)`.

Options: `:base-ms` (default `250`), `:increment-ms` (default `250`),
`:max-ms` (default `5_000`), plus the `:jitter` / `:rand-fn` pair documented
on `exponential-backoff`.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close