Skip to content

document how fetch() holds on to a caller's signal - #424

Open
orgads wants to merge 1 commit into
isaacs:mainfrom
orgads:docs-fetch-signal
Open

orgads wants to merge 1 commit into
isaacs:mainfrom
orgads:docs-fetch-signal

Conversation

@orgads

@orgads orgads commented Oct 21, 2026

Copy link
Copy Markdown

FetchOptions.signal has no documentation. When a signal is passed for an expired entry, the background fetch adds an abort listener to it with { signal: ac.signal }. Node keeps that listener alive until one of the two signals aborts. After a successful fetch neither does, so the internal AbortController, the fetch's closures and the replaced stale value stay in memory.

We hit this in production. A new AbortController per call, with a 2 s timeout as a stale fallback for a slow DB, ran the heap out after several hours. Repro on main (50 keys, 20 ms TTL, 10,000 refreshes per round): heap grows 32 → 173 MB with signal, and stays at about 5 MB with .finally(() => ac.abort()) or without signal.

This PR documents the behavior and the workaround. A code fix would release the listener on the caller's signal once the background fetch settles; I can open an issue or a PR for that if you'd prefer.

🤖 Generated with Claude Code

The background fetch registers an abort listener on the caller's
signal, and that listener keeps the fetch reachable until the signal
aborts. A signal that is never aborted after a successful fetch keeps
the stale value and the fetch's internals alive, so recommend aborting
it once the fetch settles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant