Skip to content

Retry, budgets and throttling

Retry, budgets and throttling

ts
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 byreasonMeans
breakercircuit-openthe upstream looks wholly down
limiterlimiter-fulltoo many in flight for current latency
throttlerthrottledtoo many recent attempts were not accepted
bulkheadbulkhead-fulla hard concurrency cap you configured
rate limiterrate-limiteda fixed rate you configured
budgetbudget-exceededthe 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.

MIT licensed. Zero runtime dependencies.