Skip to content

Backoff ​

Backoff is a readonly record struct that sets the delay between retry attempts.

MemberDescription
Backoff.DefaultUses Exponential() with a 100 ms transient base, 1 s throttled base, factor of 2, 30 s cap, and full jitter.
Backoff.NoneRetries immediately. Use this only when the dependency is not shared.
Backoff.Exponential(transientBase, throttledBase, factor, maximumDelay)Uses exponential backoff with separate bases for different retryable verdicts. All parameters are optional.
Backoff.Measured(multiple, transientBase, throttledBase, factor, maximumDelay)Uses exponential backoff whose transient base is measured from recent latency. All parameters are optional.
Backoff.Constant(delay)Uses the same delay before every retry.
Backoff.Custom(Func<NextAttempt, TimeSpan>)Computes the delay yourself. This mode ignores the MaximumDelay property and jitter.
JitterDetermines the amount of randomness applied to the delay.
MaximumDelayThe maximum allowable delay for any single attempt. Defaults to 30 s. Use Timeout.InfiniteTimeSpan for no cap.
TransientBaseThe base delay for a Transient verdict. Zero for a Custom curve.
ThrottledBaseThe base delay for a Throttled verdict. Zero for a Custom curve.
FactorThe growth per attempt.
KindWhich curve this is: Exponential, Constant, or Custom. Read-only; the factories are the only way to choose one.
MeasuredBaseA MeasuredBase? that measures TransientBase from recent latency, or null to keep the configured constant. Only an Exponential curve may carry one.
Compute(in NextAttempt)Calculates the delay before the specified attempt. This value is never negative.

Changing one term ​

A factory chooses the curve; with changes one term of it. Every property except Kind has an init accessor, so you never have to restate the terms you are not changing:

csharp
var slower = Backoff.Default with { MaximumDelay = TimeSpan.FromMinutes(2) };

slower.MaximumDelay;    // 00:02:00 - the term you set
slower.TransientBase;   // 00:00:00.1000000 - everything else is the shipped default
slower.Factor;          // 2
slower.Jitter;          // Jitter.Full

Kind is the exception, and deliberately: a Custom curve carries the delegate that computes its delays, and nothing but Backoff.Custom can supply one. A Kind switched away from the factory that built it would name a curve with nothing behind it.

Two things to know when you derive from a non-exponential curve:

  • Backoff.Constant(delay) sets the cap to delay as well as both bases. Raising one base above the cap without raising the cap is clamped straight back down.
  • Backoff.Custom(...) reports zero bases and no cap, and ignores every term but the delegate. Setting one with with changes what the properties report and nothing about the delays served.

Reading a backoff back ​

The five readable properties report the values Compute will actually use, so an unconstructed default(Backoff) reports the shipped defaults rather than zeros:

csharp
var backoff = default(Backoff);

backoff.Kind;           // BackoffKind.Exponential
backoff.TransientBase;  // 00:00:00.1000000
backoff.ThrottledBase;  // 00:00:01
backoff.Factor;         // 2
backoff.MaximumDelay;   // 00:00:30

The defaults are supplied on read rather than by a constructor, because policy with { Backoff = default } compiles and a struct's default instance is the one thing a constructor cannot reach. That is also why equality is over the effective curve: a value that names a default explicitly equals one that left it alone, and default(Backoff) equals Backoff.Default.

Exponential backoff calculation ​

For exponential backoff, the delay for attempt n is: base × factor^(n-2)

The result is capped at MaximumDelay and then jittered. The first retry is served the base delay.

Default parameters:

  • transientBase: 100 ms
  • throttledBase: 1 s
  • factor: 2.0
  • maximumDelay: 30 s

Priority and constraints ​

Verdict.RetryAfter wins over all backoff curves: it is honored verbatim, capped only by MaximumDelay, with no jitter.

The executor also keeps a delay from consuming the deadline's remaining time. If a delay would exceed the deadline, the call fails immediately with a deadline exception instead of sleeping.

MeasuredBase ​

MeasuredBase is a readonly record struct that measures TransientBase from what a call to this dependency recently took, instead of taking it as a constant. It is opt-in, and Retry has the argument for it.

MemberDefaultDescription
MeasuredBase.Times(multiple)1Builds a configuration. multiple is how many normal calls the first retry waits.
Multiplenone - you supply itHow many normal calls the first retry waits. Must be greater than zero.
Quantile0.5The quantile of recent successful latency that counts as normal. Must be in (0, 0.5].
Window5 minHow much history the baseline covers.
MinimumSamples20How many recent successful calls the baseline needs before it moves anything.
Spread10How far the measured base may move from TransientBase, as a factor in either direction. Must be greater than 1.

The measured base applies to TransientBase only. ThrottledBase stays the constant it was configured as, and Verdict.RetryAfter still wins over both. Value equality is over the effective configuration, so a value that names a default equals one that left it alone.

Compute(in NextAttempt) returns the unmeasured curve: the estimate is private to the policy instance that owns it, so only the executor can supply it, and a bare Backoff value answers with the configured curve - the same answer the executor gives while the estimate is still cold.

BackoffKind ​

BackoffKind identifies which curve a Backoff follows. Read it from the Kind property.

ValueCurve
ExponentialThe delay grows by Factor on each attempt. Set by Backoff.Exponential and Backoff.Default.
ConstantThe same delay every time. Set by Backoff.Constant and Backoff.None.
CustomA caller-supplied delegate computes the delay. Set by Backoff.Custom.

Jitter ​

Jitter adds randomness to the delay to prevent thundering-herd problems, where many clients retry simultaneously.

ValueResulting Delay
Fullrandom(0, computed). This is the default and the most effective way to break correlation between clients.
Equal(computed / 2) + random(0, computed / 2). This maintains a minimum delay floor.
NoneNo randomness is applied. This is typically only used in tests.

Released under the MIT License.