Skip to content

fetch

createResilientFetch() returns a function with the same signature as fetch. Every request goes through a total timeout, retries with backoff and jitter, and a circuit breaker per host. Use it for any outbound HTTP call where you would otherwise write the same retry loop again. It needs no dependencies: it wraps globalThis.fetch (Node ≥ 20, Bun, Deno, Workers, browsers).

import {
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
} from 'resilience-pipeline/fetch';
const
const http: ResilientFetch
http
=
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
({
HttpResilienceOptions<Response>.name?: string | undefined

Labels telemetry. Per-host pipelines are named <name>/<key>.

name
: 'payments' });
const
const res: Response
res
= await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
('https://api.example.com/balance');
if (!
const res: Response
res
.
Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

MDN Reference

ok
) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`HTTP ${
const res: Response
res
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
}`);
const
const balance: any
balance
= await
const res: Response
res
.
Body.json(): Promise<any>
json
();

The result is still a typeof fetch, so you can pass it to any SDK or wrapper that accepts a custom fetch option.

total timeout (30 s) → retry (3×, exponential from 200 ms, full jitter, Retry-After)
→ per host: circuit breaker → concurrency limiter (optional) → attempt timeout (10 s) → fetch
  • Total timeout bounds the whole call, retries and their delays included.
  • Retry repeats network errors, timeouts and 408 429 500 502 503 504 responses. A Retry-After header (seconds or an HTTP date) replaces the computed delay.
  • Circuit breaker per host (url.origin): opens when 50% of the last 20 calls failed (once at least 10 were seen) and stays open 10 s. A dead host doesn’t block calls to healthy ones.
  • Attempt timeout bounds each single attempt, so one hung connection doesn’t use up the total budget.

Responses come back as they are. A final 503 resolves like it does with fetch; only network errors and resilience rejections (TimeoutRejectedError, BrokenCircuitError, BulkheadRejectedError) throw. A response that is discarded for a retry has its body cancelled first, so the connection goes back to the pool instead of waiting for garbage collection.

async function
function stock(sku: string): Promise<{
stock: number;
}>
stock
(
sku: string
sku
: string) {
try {
const
const res: Response
res
= await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
(`https://inventory.example.com/stock/${
sku: string
sku
}`);
if (!
const res: Response
res
.
Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

MDN Reference

ok
) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`HTTP ${
const res: Response
res
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
}`); // 4xx, or a 5xx after the last retry
return (await
const res: Response
res
.
Body.json(): Promise<any>
json
()) as {
stock: number
stock
: number };
} catch (
function (local var) error: unknown
error
) {
if (
function isBrokenCircuitError(error: unknown): error is BrokenCircuitError
isBrokenCircuitError
(
function (local var) error: unknown
error
) ||
function isTimeoutError(error: unknown): error is TimeoutRejectedError
isTimeoutError
(
function (local var) error: unknown
error
)) return
const cached: {
stock: number;
}
cached
;
throw
function (local var) error: unknown
error
;
}
}
import {
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
} from 'resilience-pipeline/fetch';
const
const http: ResilientFetch
http
=
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
({
HttpResilienceOptions<Response>.name?: string | undefined

Labels telemetry. Per-host pipelines are named <name>/<key>.

name
: 'payments',
HttpResilienceOptions<Response>.totalTimeout?: Duration | undefined

Budget for the whole call, retries included. Default 30 s.

totalTimeout
: '15s',
HttpResilienceOptions<Response>.attemptTimeout?: Duration | undefined

Budget per attempt. Default 10 s.

attemptTimeout
: '3s',
HttpResilienceOptions<Response>.retry?: false | Omit<RetryOptions<Response>, "shouldHandle"> | undefined

Retry options merged over the defaults (3 retries, exponential backoff from 200 ms, full jitter, Retry-After honoured). false disables retries.

retry
: {
maxRetryAttempts?: number | undefined

Retries in addition to the original call. Default 3. Infinity retries until aborted.

maxRetryAttempts
: 2,
delay?: Duration | undefined

Base delay. Default 2000 ms.

delay
: 500,
maxDelay?: Duration | undefined

Caps computed delays (not those from delayGenerator).

maxDelay
: '5s' },
HttpResilienceOptions<Response>.retryOn?: RetryOn<Response> | undefined

What to retry. Default: network errors, timeouts and 408/429/500/502/503/504.

retryOn
: [429, 502, 503],
HttpResilienceOptions<Response>.circuitBreaker?: false | Omit<CircuitBreakerOptions<Response>, "shouldHandle"> | undefined

Circuit breaker options merged over the defaults (50% failures over the last 20 calls, 10 s break). false disables it.

circuitBreaker
: {
failureRatio?: number | undefined

Failure ratio in the window that breaks the circuit. Default 0.1.

failureRatio
: 0.3,
breakDuration?: Duration | undefined

How long the circuit stays open. Default 5 s.

breakDuration
: '30s' },
HttpResilienceOptions<Response>.concurrency?: number | ConcurrencyLimiterOptions | undefined

Max attempts in flight per key (bulkhead), e.g. 20 or { permitLimit: 20, queueLimit: 100 }. Default: no limit.

concurrency
: {
ConcurrencyLimiterOptions.permitLimit: number

Max executions running at once.

permitLimit
: 20,
ConcurrencyLimiterOptions.queueLimit?: number | undefined

Max executions waiting (FIFO) for a permit. Default 0.

queueLimit
: 50,
ConcurrencyLimiterOptions.queueTimeout?: Duration | undefined

Longest a queued execution waits for a permit before it is rejected. Default: no limit.

queueTimeout
: '1s' },
});
Option Type Default Description
name string none Pipeline name for telemetry. Per-host pipelines are named <name>/<key>, e.g. payments/https://api.example.com.
totalTimeout Duration '30s' Budget for the whole call, retries and delays included.
attemptTimeout Duration '10s' Budget for each attempt.
retry RetryOptions | false 3 retries, delay: 200, backoffType: 'exponential', useJitter: 'full' Merged over the defaults (every retry option except shouldHandle). false: no retries. respectRetryAfter: false ignores Retry-After.
retryOn number[] | (args) => boolean [408, 429, 500, 502, 503, 504] What to retry. A status list replaces the default statuses; network errors and timeouts are still retried. A function decides alone.
circuitBreaker CircuitBreakerOptions | false failureRatio: 0.5, samplingCount: 20, minimumThroughput: 10, breakDuration: '10s' Merged over the defaults (every breaker option except shouldHandle). false: no breaker.
concurrency number | ConcurrencyLimiterOptions no limit Max attempts in flight per key. Beyond it (and the queue), attempts are rejected with BulkheadRejectedError.
pipelineKey (url: URL) => string url => url.origin Which requests share a breaker and a limiter. () => 'all' shares one for everything.
maxPipelines number 100 Max per-key pipelines kept. The least recently used is dropped beyond it, and with it its breaker state.
retryUnsafeMethods boolean false Retry POST, PATCH and other unsafe methods without an Idempotency-Key header too.
timeProvider TimeProvider system clock For tests with FakeTimeProvider.
fetch typeof fetch globalThis.fetch The fetch to wrap (undici’s, a mock, an instrumented one).
pipeline ResiliencePipeline<Response> built in Your own pipeline instead. Per-request retries and the per-host split are then up to you.
onBeforeAttempt (request, context) => Request | void none Called before every attempt with a fresh Request; return a replacement.

retryOn as a function receives { response, error, attemptNumber }: response is set when one arrived, error when the attempt threw. The circuit breaker ignores retryOn and always counts network errors, timeouts and 408 429 500 502 503 504 as failures.

Pass resilience in the second argument. The type is ResilientRequestInit, a RequestInit with one more field.

// A health probe: one attempt, short budget.
await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
('https://api.example.com/health', {
ResilientRequestInit.resilience?: RequestResilience<Response> | undefined
resilience
: {
RequestResilience<Response>.retries?: number | false | undefined

Retries for this request. false or 0: a single attempt.

retries
: 0,
RequestResilience<Response>.timeout?: Duration | undefined

Budget for this request, retries included. Infinity disables it.

timeout
: '1s' } });
// A slow export: more time, no attempt timeout below the total.
await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
('https://api.example.com/export', {
ResilientRequestInit.resilience?: RequestResilience<Response> | undefined
resilience
: {
RequestResilience<Response>.timeout?: Duration | undefined

Budget for this request, retries included. Infinity disables it.

timeout
: '2m',
RequestResilience<Response>.attemptTimeout?: Duration | undefined

Budget per attempt for this request.

attemptTimeout
: '2m' } });
// Safe to repeat even though it's a POST (the server deduplicates by order id).
await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
('https://api.example.com/orders', {
RequestInit.method?: string | undefined

A string to set request's method.

method
: 'POST',
RequestInit.body?: BodyInit | null | undefined

A BodyInit object or null to set request's body.

body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@param ― value A JavaScript value, usually an object or array, to be converted.

@param ― replacer A function that transforms the results.

@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws ― {TypeError} If a circular reference or a BigInt value is found.

stringify
(
const order: object
order
),
ResilientRequestInit.resilience?: RequestResilience<Response> | undefined
resilience
: {
RequestResilience<Response>.idempotent?: boolean | undefined

Force (true) or forbid (false) retrying, whatever the method and headers.

idempotent
: true,
RequestResilience<Response>.retryOn?: RetryOn<Response> | undefined
retryOn
: [503] },
});
Field Type Description
retries number | false Retries for this request. 0 or false: a single attempt.
retryOn same as the option What to retry for this request.
timeout Duration Total budget for this request. Infinity disables it.
attemptTimeout Duration Budget per attempt for this request.
idempotent boolean true forces retrying, false forbids it, whatever the method and headers (a stream body is still never retried).

Requests with a different retries value still share the same per-host breakers and limiters.

A request that may have reached the server is only repeated when repeating it is safe:

  • GET, HEAD, OPTIONS, TRACE, PUT and DELETE are retried.
  • POST, PATCH and any other method are retried only with an Idempotency-Key header, resilience: { idempotent: true }, or retryUnsafeMethods: true.
  • A request whose body is a stream (ReadableStream, a Node stream or async iterable; for axios, a stream data) is never repeated once sent, whatever the rules above: the body can only be read once. The failed response or error is returned as is. Buffer the body (string, Uint8Array, Blob) to make it retryable.

A POST that timed out may have charged a card; retrying it could charge it twice. Send an Idempotency-Key when the API supports one (Stripe, Adyen and most payment APIs do):

await
const http: (input: string | URL | Request, init?: ResilientRequestInit) => Promise<Response> (+1 overload)
http
('https://api.example.com/charges', {
RequestInit.method?: string | undefined

A string to set request's method.

method
: 'POST',
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: { 'content-type': 'application/json', 'Idempotency-Key':
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
() },
RequestInit.body?: BodyInit | null | undefined

A BodyInit object or null to set request's body.

body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@param ― value A JavaScript value, usually an object or array, to be converted.

@param ― replacer A function that transforms the results.

@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws ― {TypeError} If a circular reference or a BigInt value is found.

stringify
({
amount: number
amount
: 1200,
currency: string
currency
: 'eur' }),
});

onBeforeAttempt runs before every attempt, retries included. It gets a new Request (a Request input is cloned each time, so its body can be sent again) and the execution context. Return a replacement, or nothing to send it unchanged.

const
const http: ResilientFetch
http
=
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
({
HttpResilienceOptions<Response>.name?: string | undefined

Labels telemetry. Per-host pipelines are named <name>/<key>.

name
: 'crm',
ResilientFetchOptions.onBeforeAttempt?: ((request: Request, context: ResilienceContext) => MaybePromise<Request | void>) | undefined

Called before every attempt with a fresh Request; return a replacement, e.g. with a refreshed auth token.

onBeforeAttempt
: async (
request: Request
request
, {
attemptNumber: number

0 for the first attempt, then 1, 2, … in the order attempts start. Use it for logging or per-attempt idempotency keys. One counter per execution numbers every retry and hedged attempt, however retry and hedging are nested (retry around hedging, retry inside hedged branches), so a number is never repeated within an execution. Strategies' own counters (RetryPredicateArgs.attemptNumber, OnHedgingArgs.attemptNumber) stay local.

attemptNumber
}) => {
const
const headers: Headers
headers
= new
var Headers: new (init?: HeadersInit) => Headers

The Headers interface of the Fetch API allows you to perform various actions on HTTP request and response headers.

MDN Reference

Headers
(
request: Request
request
.
Request.headers: Headers

The headers read-only property of the with the request.

MDN Reference

headers
);
const headers: Headers
headers
.
Headers.set(name: string, value: string): void

The set() method of the Headers interface sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist.

MDN Reference

set
('authorization', `Bearer ${await
function getAccessToken(): Promise<string>
getAccessToken
()}`);
const headers: Headers
headers
.
Headers.set(name: string, value: string): void

The set() method of the Headers interface sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist.

MDN Reference

set
('x-attempt',
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
attemptNumber: number

0 for the first attempt, then 1, 2, … in the order attempts start. Use it for logging or per-attempt idempotency keys. One counter per execution numbers every retry and hedged attempt, however retry and hedging are nested (retry around hedging, retry inside hedged branches), so a number is never repeated within an execution. Strategies' own counters (RetryPredicateArgs.attemptNumber, OnHedgingArgs.attemptNumber) stay local.

attemptNumber
));
return new
var Request: new (input: RequestInfo | URL, init?: RequestInit) => Request

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
(
request: Request
request
, {
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
});
},
});

To retry a 401 after the refresh, add it to retryOn: retryOn: [401, 429, 503].

  • init.signal cancels the whole call, retries included. The call rejects with the signal’s reason.
  • Each attempt gets its own signal, aborted by the attempt timeout, the total timeout or yours.
  • Called inside another pipeline (an inbound request deadline, for example), the call inherits that pipeline’s signal: when the outer budget runs out, the request is aborted. See deadline propagation.

standardHttpPipeline(options) builds the same pipeline as a plain ResiliencePipeline<Response>, for when you want to call fetch yourself or add strategies around it. isTransientHttpOutcome, parseRetryAfter and retryAfterMs are exported too, for your own predicates.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
} from 'resilience-pipeline';
import {
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
,
function isTransientHttpOutcome(args: OutcomeArgs<Response>): boolean

Network failures, timeouts and 408/429/500/502/503/504 responses.

isTransientHttpOutcome
,
function retryAfterMs(response: Response, now?: number): number | undefined

Delay from a response's Retry-After header, in ms; undefined when absent or unparseable.

retryAfterMs
} from 'resilience-pipeline/fetch';
const
const pipeline: ResiliencePipeline<Response, TimeoutRejectedError>
pipeline
= new
new ResiliencePipelineBuilder<Response, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<Response, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
<
interface Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
>({
ResiliencePipelineBuilderOptions.name?: string | undefined

Labels telemetry events.

name
: 'search' })
.
ResiliencePipelineBuilder<Response, never, never>.addRetry(options?: RetryOptions<Response>): ResiliencePipelineBuilder<Response, never, never>
addRetry
({
RetryOptions<Response>.maxRetryAttempts?: number | undefined

Retries in addition to the original call. Default 3. Infinity retries until aborted.

maxRetryAttempts
: 2,
RetryOptions<Response>.shouldHandle?: ShouldHandle<RetryPredicateArgs<Response>> | undefined

Default: transient errors (not 4xx except 408/429, not aborts, not rejections). An open circuit is never retried.

shouldHandle
:
function isTransientHttpOutcome(args: OutcomeArgs<Response>): boolean

Network failures, timeouts and 408/429/500/502/503/504 responses.

isTransientHttpOutcome
,
RetryOptions<Response>.delayGenerator?: ((args: RetryPredicateArgs<Response>) => MaybePromise<Duration | undefined>) | undefined

Return a delay (e.g. from a Retry-After header); undefined or negative falls back to the computed delay.

delayGenerator
: ({
outcome: Outcome<Response, unknown>
outcome
}) => (
outcome: Outcome<Response, unknown>
outcome
.
ok: boolean
ok
?
function retryAfterMs(response: Response, now?: number): number | undefined

Delay from a response's Retry-After header, in ms; undefined when absent or unparseable.

retryAfterMs
(
outcome: {
readonly ok: true;
readonly result: Response;
}
outcome
.
result: Response
result
) :
var undefined
undefined
),
})
.
ResiliencePipelineBuilder<Response, never, never>.addTimeout(options: Duration | TimeoutOptions): Next<Response, never, never, TimeoutRejectedError>

addTimeout(5000), addTimeout('5s') or addTimeout({ timeout: '5s', onTimeout }).

addTimeout
('2s')
.
ResiliencePipelineBuilder<Response, TimeoutRejectedError, never>.build(): ResiliencePipeline<Response, TimeoutRejectedError>
build
();
const
const http: ResilientFetch
http
=
function createResilientFetch(options?: ResilientFetchOptions): ResilientFetch

A drop-in fetch with timeouts, retries and a circuit breaker per host: const http = createResilientFetch({ name: 'payments' }); const res = await http(url, { resilience: { retries: 1 } }); Responses are returned as-is (a final 503 resolves, like fetch); only network errors and resilience rejections throw.

createResilientFetch
({
ResilientFetchOptions.pipeline?: ResiliencePipeline<Response, any> | undefined

Use your own pipeline instead of the built-in one (per-request retries, timeout, retryOn and per-host keys are then ignored).

pipeline
});