backoff

package
v0.26.22 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 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.

How the randomisation is laid on is Value.JitterMode: added to the curve (the zero, JitterAdditive), drawn across all of it (JitterFull), or drawn from the previous wait (JitterDecorrelated) — the three variants of the AWS Builders' Library's "Timeouts, retries and backoff with jitter".

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Decorrelate added in v0.21.0

func Decorrelate(base, ceiling, previous time.Duration) time.Duration

Decorrelate returns the next wait of a "Decorrelated Jitter" backoff (AWS Builders' Library): drawn uniformly from [base, 3 × previous], held at ceiling when one is set (a zero ceiling is none) and at the longest time.Duration otherwise. A previous wait below base is read as base, so the first wait of a loop is drawn from [base, 3 × base]; a non-positive base is no wait.

It is the one variant whose wait depends on the previous wait rather than on the attempt, so the loop keeps that wait and hands it back: the bound grows with what was actually waited, and two callers that failed together drift apart instead of following one curve.

func Full added in v0.21.0

func Full(delay time.Duration) time.Duration

Full returns a wait drawn uniformly from [0, delay]: the "Full Jitter" of the AWS Builders' Library, laid on a delay the caller already grew and held at its ceiling. A zero or negative delay is no wait.

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 JitterMode added in v0.21.0

type JitterMode uint8

JitterMode is how a Value randomises its waits: JitterAdditive (the zero), JitterFull or JitterDecorrelated.

const (
	// JitterAdditive widens the grown wait by a random fraction of itself,
	// drawn from [0, Jitter) and ADDED: the average wait keeps growing with
	// the attempt. A zero Jitter is the deterministic curve. It is the zero
	// JitterMode.
	JitterAdditive JitterMode = iota
	// JitterFull draws the wait uniformly from [0, grown], the grown wait being
	// the curve held at MaxDelay: the "Full Jitter" of the Builders' Library,
	// which spreads callers that failed together the most widely of the three
	// and keeps the average at half the curve. Jitter is not read.
	JitterFull
	// JitterDecorrelated draws each wait from the previous one, as [Decorrelate]
	// does: uniformly from [BaseDelay, 3 × previous], held at MaxDelay — the
	// "Decorrelated Jitter" of the Builders' Library. A loop that keeps the
	// previous wait calls Decorrelate; [Value.Delay], which has no memory,
	// reads the curve's own previous step as the previous wait. Jitter is not
	// read.
	JitterDecorrelated
)

The three ways a Value lays its randomisation on the curve. The zero is the additive jitter the curve has always had, so a Value written before the mode existed waits exactly as it did (ADR 0031).

func (JitterMode) String added in v0.21.0

func (m JitterMode) String() string

String renders the mode for a log line or a configuration dump: "additive", "full", "decorrelated"; a value this package does not define renders "unknown".

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.
	//
	// It is read only under JitterAdditive.
	Jitter float64
	// JitterMode is how the randomisation is laid on the curve. The zero,
	// JitterAdditive, is the Jitter above — what Value has always done;
	// JitterFull and JitterDecorrelated are the Builders' Library's two
	// other variants, which randomise the whole wait and ignore Jitter. A
	// value outside the three is read as JitterAdditive.
	JitterMode JitterMode
}

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;
  • a JitterMode this package does not define is read as JitterAdditive.

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 the randomisation JitterMode names — Widen by Jitter for JitterAdditive, Full for JitterFull, and for JitterDecorrelated Decorrelate, reading the curve's wait for the attempt before as the previous wait (a loop that keeps its own calls Decorrelate). 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