Skip to content

Services Retry

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

Retry

io.retry retries failed requests with exponential backoff, but only when it is safe to do so: idempotent verbs retry automatically, while a bare POST does not.

import io from 'double-meh';

io.retry.attach();
const data = await io.get('https://example.com/r', null, {retries: 3});
// a 5xx / 429 / network error is retried up to 3 times, then the last result stands

Retries are off unless you pass retries (a number >= 1). What counts as a failure is a network error, any 5xx, or 429.

Verb-aware safety

GET, HEAD, OPTIONS, PUT, and DELETE are idempotent and retry automatically. PATCH retries only with ifMatch or as: 'merge-patch'. A bare POST is not retried; it must carry an idempotencyKey or If-None-Match: * (ifNoneMatch: '*') to unlock retry. Set retry: true to force retry regardless, or retry: false to disable it.

// bare POST: not retried even with retries set
await io.post('https://example.com/p', {a: 1}, {retries: 3, ignoreBadStatus: true});

// POST with an idempotency key: retried, same key reused across attempts
await io.post('https://example.com/p', {a: 1}, {retries: 1, idempotencyKey: true});

Honours Retry-After

When a failed response carries a Retry-After header (delta-seconds or an HTTP date), that delay is used for the next attempt instead of the backoff timer.

await io.get('https://example.com/rate-limited', null, {retries: 2});
// a 429 with `Retry-After: 5` waits ~5s before retrying

Retried DELETE 404 becomes 204

If a DELETE succeeds on the server but its response is lost and a retry then returns 404, the resource is already gone — so a 404 on any attempt after the first is rewritten to 204 No Content.

const env = await io.full.delete('https://example.com/d', null, {retries: 3});
env.status; // 204 when a retried DELETE hits 404

Backoff options

  • retries — maximum retry attempts (required to enable retry).
  • retrytrue forces retry, false disables it.
  • initDelay — first backoff delay in ms (default io.retry.initDelay, 100). Use initDelay: 0 for immediate retries.
await io.get('https://example.com/r', null, {retries: 3, initDelay: 200});

nextDelay

io.retry.nextDelay(delay, attempt, options) computes the next backoff from the current one; the default doubles it and caps at 5s. Replace it to change the curve.

io.retry.nextDelay = delay => Math.min(delay * 3, 10_000);

Requests with a streaming (ReadableStream) body are not retried, since the body cannot be replayed.

Attaching

io.retry.attach() installs the service (setting io.retry.isActive); io.retry.detach() removes it. Both return io.

io.retry.attach();
io.retry.detach();

See also

Clone this wiki locally