Skip to content

Removed cache entries retain or recreate garbage-collection timers after clear() #12561

Open

Description

@rvitaliy

Describe the bug

QueryClient.clear() empties both caches, but garbage-collection timers can outlive the entries they were meant to remove:

  1. MutationCache.clear()/remove() remove a mutation without clearing its existing GC timer.
  2. QueryCache.remove() destroys a query and clears its timer, but an in-flight fetch cancellation later runs its finally block and schedules GC again. Removing the last observer after removal can also recreate a timer.

In the reproductions below, the residual timer retains the entry for gcTime (five minutes with browser defaults). An unbounded memory leak has not been measured. A removed mutation that is still pending can repeatedly schedule GC until it settles. The caches are empty; the timers are still live.

Your minimal, reproducible example

reproduction.mjs

Save the file in an empty directory, then run:

npm install --no-save @tanstack/query-core@5.107.1
node reproduction.mjs

It uses @tanstack/query-core 5.107.1 and its public timeout provider to count live timers without waiting for them. The observer/query classes are real; no request, framework app, or private source is required. Regressions in packages/query-core/src/tests/cacheGc.test.tsx exercise the same behavior with the upstream test runner.

Steps to reproduce

  • Complete a mutation, reset its observer so it is unused, then call client.clear(). Its cache is empty but a 300000 ms timer remains.
  • Start a pending query with an observer that consumes its AbortSignal, then clear the client. The signal aborts and the cache empties, but once cancellation settles another 300000 ms GC timer appears. Later observer removal can also schedule it again.
  • Explicitly destroying the removed entries after these continuations clears the residual timers.

Expected behavior

Removing an entry should release its existing GC timer and should not let later cancellation or observer cleanup schedule another timer for that removed entry. A replacement query with the same key must retain its own normal GC lifecycle.

This does not require cancelling running mutations, changing gcTime defaults, or disabling server-side GC. Cached entries, reset/refetch and entries re-added and used through their normal observer lifecycle should still be collected normally.

How often does this bug happen?

Every time in the isolated reproductions.

Platform

macOS arm64. The library regression tests run on Node.js 24.22.1 using the repository's jsdom environment; the standalone reproduction counts timers with a custom TimeoutProvider rather than a browser heap snapshot.

TanStack Query adapter and version

Core/vanilla reproduction; our consumer uses react-query. @tanstack/query-core and @tanstack/react-query 5.107.1. Also reproduced on the current main checkout, commit 8f6da1e. TypeScript 6.0.3 for the local checkout.

Additional context

Our test teardown currently retains references to cache entries even after inline clear(), awaits query cancellation, then explicitly destroys queries and mutations. Releasing removed entries' timers in the library would let us simplify that cleanup. No application-level latency or memory improvement has been measured.

Related: #12025 and closed/unmerged #12053 proposed disabling GC timers on the server. This report is narrower: release timers for entries explicitly removed from a cache, while keeping normal finite gcTime behavior on both client and server.

A fix is prepared with 14 regression cases. The complete query-core target passes 1,654 tests, and the repository test:pr gate passes all 627 selected tasks across 194 projects (24 valid Nx cache hits). The patch includes a changeset.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions