From ac3f37e5cdc91951ed2b9f7b5c3db1d3c184d22a Mon Sep 17 00:00:00 2001 From: AdzerKI Date: Fri, 28 Aug 2026 09:34:28 +0300 Subject: [PATCH] fix(query-core): do not schedule garbage collection on the server --- .changeset/quiet-timers-rest.md | 5 +++++ .../query-core/src/__tests__/query.test.tsx | 20 +++++++++++++++++++ packages/query-core/src/removable.ts | 4 ++++ 3 files changed, 29 insertions(+) create mode 100644 .changeset/quiet-timers-rest.md diff --git a/.changeset/quiet-timers-rest.md b/.changeset/quiet-timers-rest.md new file mode 100644 index 00000000000..389014363ca --- /dev/null +++ b/.changeset/quiet-timers-rest.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Do not schedule garbage collection on the server. A timer scheduled during server rendering captures the async context it was created in and keeps that whole render alive until it fires, while the client it would clean up is dropped with the response. Only queries and mutations with an explicit finite `gcTime` were affected, since the server default is already `Infinity`. diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index 669fc42ac3c..d05b9b2016b 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -13,6 +13,7 @@ import { dehydrate, hydrate, noop, + timeoutManager, } from '..' import { hashQueryKeyByOptions } from '../utils' import { mockOnlineManagerIsOnline, setIsServer } from './utils' @@ -1018,6 +1019,25 @@ describe('query', () => { } }) + it('should not schedule garbage collection on the server, even with an explicit gcTime', () => { + const resetIsServer = setIsServer(true) + const scheduled = vi.spyOn(timeoutManager, 'setTimeout') + + try { + const query = queryCache.build(queryClient, { + queryKey: queryKey(), + queryFn: () => 'data', + gcTime: 1000, + }) + + expect(query.gcTime).toBe(1000) + expect(scheduled).not.toHaveBeenCalled() + } finally { + scheduled.mockRestore() + resetIsServer() + } + }) + it('constructor should call initialDataUpdatedAt if defined as a function', async () => { const key = queryKey() diff --git a/packages/query-core/src/removable.ts b/packages/query-core/src/removable.ts index 4e84ee62060..c63d5aee292 100644 --- a/packages/query-core/src/removable.ts +++ b/packages/query-core/src/removable.ts @@ -14,6 +14,10 @@ export abstract class Removable { protected scheduleGc(): void { this.clearGcTimeout() + if (isServerEnvironment()) { + return + } + if (isValidTimeout(this.gcTime)) { this.#gcTimeout = timeoutManager.setTimeout(() => { this.optionalRemove()