Retry, budgets and throttling
Retry, budgets and throttling
import { pipeline, breaker, limiter, throttler, budget } from "resilix";
const shared = budget({ ratio: 0.1 }); // ONE instance for the whole process
const api = pipeline({
policies: [throttler(), breaker({ slowCallMs: 3_000 }), limiter()],
retry: { maxAttempts: 3, jitter: "full", budget: shared },
timeoutMs: 10_000,
});Retries are an amplifier. Three attempts per request turns a degraded upstream into a 3× load spike exactly when it can least absorb one. A 10% budget holds that to ~1.1× — Google SRE's number, and one this repo reproduces in a test rather than quoting.
The budget is a shared object. A per-pipeline cap cannot bound system-wide amplification, which is the entire point of having one.
Which failures are retried falls out of the verdict model: answered never (the upstream worked, the caller was wrong), rejected never (we refused it), transient / timeout / overload yes — and overload waits for the upstream's own Retry-After in preference to any backoff curve.
timeoutMs bounds the whole sequence, not each attempt. Most libraries bound each attempt, so a caller asking for 50 ms can wait maxAttempts × (50 ms + backoff). A deadline the caller cannot see is not a deadline.
Four ways to be refused
| Refused by | reason | Means |
|---|---|---|
| breaker | circuit-open | the upstream looks wholly down |
| limiter | limiter-full | too many in flight for current latency |
| throttler | throttled | too many recent attempts were not accepted |
| bulkhead | bulkhead-full | a hard concurrency cap you configured |
| rate limiter | rate-limited | a fixed rate you configured |
| budget | budget-exceeded | the retry was refused; the first attempt was not |
Every one arrives as RejectedError.reason and on onRejection, so "why was I refused?" always has an answer.
Going deeper
Jitter strategies, retry budget arithmetic and Google SRE's client-side throttling formula are worked through in the retry and throttling spec.