unjs / unjs/ofetch

Exponential backoff with jitter for retries

Open
#581 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
5.4k
Forks
195
PR merge metrics
No merged PRs in 30d

Description

Summary

Add a new retryBackoff option that lets users configure exponential backoff with jitter for automatic retries, following the strategies described in the AWS Architecture Blog post "Exponential Backoff And Jitter" by Marc Brooker.

Motivation

The current retry mechanism (src/fetch.ts) only supports a fixed retryDelay (a number or a callback returning a number). This is fine for a single client, but it scales poorly when many clients fail simultaneously:

  1. With no delay (retryDelay: 0, the default), every client retries immediately and stampedes the server while it is still recovering.
  2. With a fixed delay (e.g. 500ms), all failed clients still retry in lockstep. the herd is preserved, just shifted in time. The server sees the same pulsing load pattern.

This is the classic "Thundering Herd" problem. The AWS post shows via simulation that adding exponential backoff alone is not enough; the key insight is that randomizing each client's retry schedule (jitter) is what actually decorrelates the herd. With jitter, both the peak load on the server and the overall completion time across all clients improve significantly.

Today, users who want this behavior have to implement it themselves inside a retryDelay callback, which is error-prone (need to track attempt count, pick the right formula, manage prev_sleep for decorrelated jitter, etc.). It would be much nicer if ofetch provided this out of the box.

Current behavior

// src/types.ts
retry?: number | false;
retryDelay?: number | ((context: FetchContext<T, R>) => number);
retryStatusCodes?: number[];
// src/fetch.ts (onError)
const retryDelay =
  typeof context.options.retryDelay === "function"
    ? context.options.retryDelay(context)
    : context.options.retryDelay || 0;

The FetchContext does not expose the current attempt number, so even a user-defined callback cannot easily implement backoff without external state.

Proposal

Introduce a new opt-in option retryBackoff that selects a backoff strategy and supplies base / cap delays in milliseconds:

await ofetch("/api", {
  retry: 5,
  retryBackoff: {
    strategy: "full-jitter", // "full-jitter" | "equal-jitter" | "decorrelated-jitter"
    base: 100,  // minimum delay (ms)
    cap: 3000,  // maximum delay (ms)
  },
});

Strategies

All three strategies from the AWS post would be supported so users can pick the one that matches their constraints:

  • full-jitter (recommended in the AWS post):
    sleep = random(0, min(cap, base * 2 ** attempt))
    Best overall — lowest server load and lowest completion time in the simulation. No state required.

  • equal-jitter:
    temp = min(cap, base * 2 ** attempt); sleep = temp / 2 + random(0, temp / 2)
    Guarantees a minimum wait time (temp / 2), useful when "retry immediately" is undesirable.

  • decorrelated-jitter:
    sleep = min(cap, random(base, prev_sleep * 3)) (with prev_sleep = base on the first retry)
    Comparable to full-jitter in performance and matches the AWS SDK's default. Requires tracking the previous sleep value.

Expose attempt count on the context

To keep callback-based use cases working and to let users observe retries, add a public retryAttempt?: number field to FetchContext:

  • undefined on the first attempt
  • 1, 2, ... on subsequent retries

Coexistence with retryDelay

When both retryBackoff and retryDelay are set, retryBackoff takes precedence and retryDelay is ignored. Documented explicitly to avoid silent mixing of the two semantics.

Backward compatibility

Fully opt-in. If retryBackoff is not specified, the existing retryDelay behavior is preserved exactly. No default delay is applied, no public type changes are breaking.

References

Additional information
  • Would you be willing to help implement this feature?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the retry handling in src/fetch.ts, the retry option types in src/types.ts, and the “Auto Retry” section of README.md. Trace how attempts and retryDelay are currently handled, then define the three proposed backoff strategies, their precedence, context field, documentation, and opt-in backward-compatible behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.