Skip to content

Cookbook: retries

Eugene Lazutkin edited this page Jul 1, 2026 · 2 revisions

Cookbook: Retries

The retry service re-issues failed requests with exponential backoff — but only when it is safe to do so. It is attached by default and off until a request asks for retries. A retryable failure is a network error, a 5xx, or a 429.

Turn retries on

Set retries to the number of extra attempts. Without it, nothing is retried.

import io from 'double-meh';

// initial call + up to 3 retries
const data = await io.get('https://api.example.com/flaky', null, {retries: 3});

Verb-aware safety

Retries only fire for requests that are safe to repeat. GET, HEAD, OPTIONS, PUT, and DELETE are always retried. A bare POST is not — a network blip could mean the first attempt already succeeded, so blindly resending risks a duplicate.

// not retried: repeating a POST could create two resources
await io.post('https://api.example.com/orders', order, {retries: 3});

Make a POST safely retryable

Give the server a way to deduplicate. idempotencyKey: true generates a UUID once and reuses it across every retry; a string uses your own key. (ifNoneMatch: '*' also unlocks retries for create-if-absent POSTs.)

await io.post('https://api.example.com/orders', order, {
  retries: 3,
  idempotencyKey: true // one Idempotency-Key header, reused on each attempt
});

A PATCH becomes retryable with ifMatch or as: 'merge-patch'. To override the safety gate entirely, force it with retry: true; disable per request with retry: false.

Honor Retry-After

When a 429 or 503 carries a Retry-After header, the service waits exactly that long — seconds or an HTTP date — instead of using its own backoff.

await io.get('https://api.example.com/rate-limited', null, {retries: 5});
// a 429 with `Retry-After: 2` pauses 2s before the next attempt

A retried DELETE that 404s is a success

If a DELETE is retried and a later attempt returns 404, the resource is already gone — the service reports 204 No Content rather than surfacing the 404.

const env = await io.full.delete('https://api.example.com/things/1', null, {retries: 3});
env.status; // 204 even if the retry saw a 404

Tune the backoff

initDelay sets the first pause (default 100 ms; use 0 in tests). io.retry.nextDelay(delay, attempt, options) computes each subsequent pause — the default doubles it, capped at 5 s. Replace it for custom backoff or jitter.

await io.get(url, null, {retries: 4, initDelay: 250});

io.retry.initDelay = 200;
io.retry.nextDelay = (delay, attempt) => Math.min(delay * 2, 10000) + Math.random() * 100;

Note: a request whose body is a ReadableStream is never retried — the stream cannot be replayed.

See also

Clone this wiki locally