backoff

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 3 Imported by: 0

README

backoff

The SDK's one exponential backoff curve — stdlib-only, no domain vocabulary. The wait after the n-th consecutive failure is BaseDelay × Multiplier^(n−1), held at MaxDelay, widened by Jitter:

curve := backoff.Value{BaseDelay: time.Second, MaxDelay: time.Minute}

wait := curve.Delay(failures) // 1s, 2s, 4s, … 1m, 1m, …

Three edges worth knowing before you use it:

  • It never returns a negative duration, however large the attempt: the growth is compared with its bound before it is converted, so it stops at the ceiling — or at the longest time.Duration — instead of wrapping.
  • Nothing is refused, everything is normalised. A Multiplier of 1 or below (or NaN) doubles; a Jitter outside [0, 1] is clamped and NaN is none; a zero BaseDelay waits nothing.
  • Jitter is ADDED after the ceiling, so with one set a wait may reach MaxDelay × (1 + Jitter). Zero jitter is the deterministic curve.

Grow and Widen are the two halves Delay composes — the pure growth and the randomisation — for a caller that needs to assert one without the other. It is published as pkg/v1/app/resilience.Backoff. See CLAUDE.md for the design.

Documentation

Overview

Package backoff computes an exponential backoff: how long a loop that keeps failing waits before its next attempt. It is a kernel primitive — stdlib-only and domain-neutral: a duration, a growth factor, a ceiling and a jitter, with no Retry, no Job and no Delivery in a signature.

One curve serves every loop that backs off. The retry policy waits it between attempts; a supervised goroutine waits it between restarts, an outbox between deliveries, a state machine between failed transitions, a server between failed accepts. Each of those used to compute its own copy, and the copy that lived in the retry policy carried a defect the shared one fixes (ADR 0103): see Value for the float-to-duration conversion that made amd64 retries stop waiting at attempt 35.

The curve is two claims kept apart on purpose. Grow is the deterministic growth, a PURE function of the attempt; Widen is the randomisation laid on top of it. Value.Delay composes the two. A suite that cannot assert the growth without the jitter can assert neither precisely, and a caller that wants the deterministic curve asks for it by name.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Grow

func Grow(base, ceiling time.Duration, multiplier float64, attempt int) time.Duration

Grow returns the deterministic half of the curve: base × multiplier^(attempt−1), held at ceiling when one is set (a zero ceiling is none) and at the longest time.Duration otherwise. It is a PURE function of its arguments.

multiplier is normalised as NormalMultiplier does, and attempt is read as at least 1, so no input makes it return a negative duration or loop past the few dozen multiplications that reach the bound.

func NormalJitter

func NormalJitter(jitter float64) float64

NormalJitter clamps a jitter fraction into [0, 1], reading NaN as none.

NaN is resolved FIRST because Go's min and max propagate it: min(max(NaN, 0), 1) is NaN, and NaN <= 0 is false, so it would sail past every later guard into a float-to-integer conversion Go leaves implementation-defined.

func NormalMultiplier

func NormalMultiplier(multiplier float64) float64

NormalMultiplier resolves a growth factor that would not grow — 1 or below, or NaN — to the doubling default, and returns any other factor unchanged, +Inf included: Grow holds an infinite growth at the ceiling.

func Widen

func Widen(delay time.Duration, jitter float64) time.Duration

Widen returns delay widened by a uniform random fraction of itself, drawn from [0, jitter) and ADDED, so the average wait keeps growing with the attempt rather than being pulled back toward zero.

jitter is normalised as NormalJitter does; zero hands delay back unchanged, and so does a delay that is zero, negative or too small to split. The widened wait stays representable: it is spread across what is left below the longest time.Duration rather than past it.

Types

type Value

type Value struct {
	// BaseDelay is the wait after the first failure. Zero or negative waits
	// nothing.
	BaseDelay time.Duration
	// MaxDelay is the ceiling on the grown delay. Zero means no ceiling.
	MaxDelay time.Duration
	// Multiplier is the growth factor between consecutive waits. 1 or below,
	// and NaN, mean 2.
	Multiplier float64
	// Jitter widens each wait by a uniform random fraction of itself, drawn
	// from [0, Jitter) and ADDED, after the MaxDelay ceiling — so when a
	// ceiling is set a wait may reach MaxDelay × (1 + Jitter). Zero is the
	// deterministic backoff.
	//
	// It exists because a deterministic backoff SYNCHRONISES the very callers
	// it is meant to spread: N clients that fail against one dependency at the
	// same instant retry at the same instant, N times over. Jitter turns those
	// bursts back into a distribution.
	Jitter float64
}

Value is an exponential backoff: the wait after the n-th consecutive failure is BaseDelay × Multiplier^(n−1), held at MaxDelay when one is set and widened by Jitter when one is asked for.

The zero value is usable and waits nothing: a zero BaseDelay retries at once. Every other field is normalised on each call, so no value can make Delay misbehave:

  • a Multiplier of 1 or below, or NaN, grows by 2 — a flat or shrinking "exponential" backoff is a fixed-interval hammer on a dependency that is already struggling;
  • a zero MaxDelay is no ceiling, and the growth then stops at the longest time.Duration rather than wrapping negative;
  • a Jitter outside [0, 1], or NaN, is clamped into it.

The growth is compared against its bound in the float domain BEFORE any conversion. The copy this replaces converted first: past 2^63 nanoseconds Go leaves that conversion implementation-defined — math.MinInt64 on amd64, math.MaxInt64 on arm64 — and a negative wait is below every ceiling, so a one-second retry on amd64 stopped backing off at attempt 35.

func (Value) Delay

func (b Value) Delay(attempt int) time.Duration

Delay returns the wait after the attempt-th consecutive failure: Grow for the curve, then Widen for the jitter. attempt counts from 1; a smaller value is read as 1, so a loop that has not failed yet and asks anyway waits BaseDelay rather than nothing.

It never returns a negative duration, and it costs at most as many multiplications as it takes the delay to reach its ceiling, however large attempt is.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL