From e91e834e8875d8052c7990a0d3ba8dd41fc64d70 Mon Sep 17 00:00:00 2001 From: Rohit Kashyap Date: Fri, 13 Nov 2026 00:19:09 +0530 Subject: [PATCH 1/3] docs(react/guides/ssr): explain why a finite 'gcTime' keeps requests in memory on the server Co-Authored-By: Claude Opus 5.5 --- docs/framework/react/guides/ssr.md | 35 ++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 5b64f79eedf..4553736a829 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -567,13 +567,40 @@ This refetching of stale queries is a perfect match when caching markup in a CDN In case you are creating the `QueryClient` for every request, React Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. -On the server, `gcTime` defaults to `Infinity` which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity `gcTime` then you will be responsible for clearing the cache early. +On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed together with the `QueryClient` once nothing references it after the request has finished. -Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter `gcTime`, we recommend setting it to `2 * 1000` to allow sufficient time for the app to reference the data. +Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in, and frameworks such as Next.js store the whole request in that context. So with a finite `gcTime`, each request can stay in memory for the full `gcTime`, even after the response has been sent. -To clear the cache after it is not needed and to lower memory consumption, you can add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. +This is easy to run into by accident: a `gcTime` set in `defaultOptions` applies on the server as well, so tuning it for the browser also replaces the `Infinity` default on the server: -Alternatively, you can set a smaller `gcTime`. +```tsx +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // This also applies on the server + gcTime: 1000 * 60, + }, + }, +}) +``` + +If you only want a custom `gcTime` in the browser, keep `Infinity` on the server: + +```tsx +import { environmentManager, QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: environmentManager.isServer() ? Infinity : 1000 * 60, + }, + }, +}) +``` + +Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` allows sufficient time for the app to reference the data, while limiting how long each request is kept in memory. + +If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. ### Caveat for Next.js rewrites From 476c3c412cfb2cf3ca78ca4a3622ccdbf1b4283b Mon Sep 17 00:00:00 2001 From: Rohit Kashyap Date: Fri, 13 Nov 2026 02:45:09 +0530 Subject: [PATCH 2/3] docs(ssr): explain why a finite 'gcTime' keeps requests in memory on the server Apply the same explanation to the React, Preact, Vue and Solid SSR guides, and fix the Solid guide, which described the server default backwards. Co-Authored-By: Claude Opus 5.5 --- docs/framework/preact/guides/ssr.md | 35 ++++++++++++++++++++++++----- docs/framework/react/guides/ssr.md | 10 ++++----- docs/framework/solid/guides/ssr.md | 33 ++++++++++++++++++++++++++- docs/framework/vue/guides/ssr.md | 33 ++++++++++++++++++++++++--- 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/docs/framework/preact/guides/ssr.md b/docs/framework/preact/guides/ssr.md index 329d0a312be..36341b1d949 100644 --- a/docs/framework/preact/guides/ssr.md +++ b/docs/framework/preact/guides/ssr.md @@ -489,12 +489,37 @@ This refetching of stale queries is a perfect match when caching markup in a CDN ### High memory consumption on server -In case you are creating the `QueryClient` for every request, Preact Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. +On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed from memory together with the `QueryClient` once the request finishes. -On the server, `gcTime` defaults to `Infinity` which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity `gcTime` then you will be responsible for clearing the cache early. +Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in. If your server framework stores the request in that context, the request stays in memory even after the response is sent, until the timer fires. -Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter `gcTime`, we recommend setting it to `2 * 1000` to allow sufficient time for the app to reference the data. +This is easy to miss: a `gcTime` set in `defaultOptions` applies on the server too, so a value intended for the client also replaces `Infinity` on the server. -To clear the cache after it is not needed and to lower memory consumption, you can add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. +```tsx +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // This also applies on the server + gcTime: 1000 * 60, + }, + }, +}) +``` + +If you only want a custom `gcTime` in the browser, keep `Infinity` on the server: + +```tsx +import { environmentManager, QueryClient } from '@tanstack/preact-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: environmentManager.isServer() ? Infinity : 1000 * 60, + }, + }, +}) +``` + +Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` gives the app enough time to reference the data, while limiting how long each request stays in memory. -Alternatively, you can set a smaller `gcTime`. +If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 4553736a829..45c73916403 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -565,13 +565,11 @@ This refetching of stale queries is a perfect match when caching markup in a CDN ### High memory consumption on server -In case you are creating the `QueryClient` for every request, React Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. +On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed from memory together with the `QueryClient` once the request finishes. -On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed together with the `QueryClient` once nothing references it after the request has finished. +Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in, and frameworks like Next.js store the whole request in that context. So with a finite `gcTime`, the request stays in memory even after the response is sent, until the timer fires. -Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in, and frameworks such as Next.js store the whole request in that context. So with a finite `gcTime`, each request can stay in memory for the full `gcTime`, even after the response has been sent. - -This is easy to run into by accident: a `gcTime` set in `defaultOptions` applies on the server as well, so tuning it for the browser also replaces the `Infinity` default on the server: +This is easy to miss: a `gcTime` set in `defaultOptions` applies on the server too, so a value intended for the client also replaces `Infinity` on the server. ```tsx const queryClient = new QueryClient({ @@ -598,7 +596,7 @@ const queryClient = new QueryClient({ }) ``` -Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` allows sufficient time for the app to reference the data, while limiting how long each request is kept in memory. +Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` gives the app enough time to reference the data, while limiting how long each request stays in memory. If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. diff --git a/docs/framework/solid/guides/ssr.md b/docs/framework/solid/guides/ssr.md index fdced1849f3..aeef299ac9a 100644 --- a/docs/framework/solid/guides/ssr.md +++ b/docs/framework/solid/guides/ssr.md @@ -194,4 +194,35 @@ A query's staleness is based on `dataUpdatedAt`, which is set to when it resolve ### High memory consumption on server -Because the `QueryClient` is created fresh for every request, its cache is held in memory for the duration of that request. Under high request volume this can add up. `gcTime` defaults to `Infinity` on the server, so unless you configure it lower, memory for a request's cache isn't reclaimed automatically — in most cases this is fine, since the whole `QueryClient` instance becomes eligible for garbage collection once the request finishes and nothing references it anymore. +On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed from memory together with the `QueryClient` once the request finishes. + +Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in. If your server framework stores the request in that context, the request stays in memory even after the response is sent, until the timer fires. + +This is easy to miss: a `gcTime` set in `defaultOptions` applies on the server too, so a value intended for the client also replaces `Infinity` on the server. + +```tsx +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // This also applies on the server + gcTime: 1000 * 60, + }, + }, +}) +``` + +If you only want a custom `gcTime` in the browser, keep `Infinity` on the server: + +```tsx +import { environmentManager, QueryClient } from '@tanstack/solid-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: environmentManager.isServer() ? Infinity : 1000 * 60, + }, + }, +}) +``` + +If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index 716c2a6353e..3f6ac84cb2f 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -282,11 +282,38 @@ onServerPrefetch(async () => { ### High memory consumption on server -In case you are creating the `QueryClient` for every request, Vue Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. +On the server, `gcTime` defaults to `Infinity`. With `Infinity`, no garbage collection timer is scheduled, and the cache is freed from memory together with the `QueryClient` once the request finishes. -On the server, `gcTime` defaults to `Infinity` which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity `gcTime` then you will be responsible for clearing the cache early. +Any finite `gcTime` schedules a timer for each query, and that timer keeps the query in memory until it fires. In Node.js, a timer also keeps alive the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context it was created in. If your server framework stores the request in that context, the request stays in memory even after the response is sent, until the timer fires. -To clear the cache after it is not needed and to lower memory consumption, you can add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. +This is easy to miss: a `gcTime` set in `defaultOptions` applies on the server too, so a value intended for the client also replaces `Infinity` on the server. + +```ts +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // This also applies on the server + gcTime: 1000 * 60, + }, + }, +}) +``` + +If you only want a custom `gcTime` in the browser, keep `Infinity` on the server: + +```ts +import { environmentManager, QueryClient } from '@tanstack/vue-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: environmentManager.isServer() ? Infinity : 1000 * 60, + }, + }, +}) +``` + +If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. ## `dehydrate`/`hydrate` options From 90d79e5a6354b810aab71c09725b05abbdf2e64e Mon Sep 17 00:00:00 2001 From: Rohit Kashyap Date: Sat, 14 Nov 2026 19:20:53 +0530 Subject: [PATCH 3/3] docs(ssr): clarify finite gcTime risks and when to call clear() Co-Authored-By: Claude Opus 5.5 --- docs/framework/preact/guides/ssr.md | 4 ++-- docs/framework/react/guides/ssr.md | 4 ++-- docs/framework/solid/guides/ssr.md | 2 +- docs/framework/vue/guides/ssr.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/framework/preact/guides/ssr.md b/docs/framework/preact/guides/ssr.md index 36341b1d949..babb2423539 100644 --- a/docs/framework/preact/guides/ssr.md +++ b/docs/framework/preact/guides/ssr.md @@ -520,6 +520,6 @@ const queryClient = new QueryClient({ }) ``` -Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` gives the app enough time to reference the data, while limiting how long each request stays in memory. +Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. Short finite values have the same risk: a prefetched query that finishes early can be removed once its `gcTime` passes, while a slower query is still being prefetched and before `dehydrate` runs. Keep `Infinity` on the server where you can. -If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. +If you set a finite `gcTime` on the server, call [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) once you no longer need the client and all of its queries have settled, for example after you have dehydrated or rendered with it. This removes every query from the cache and cancels its pending timer. A fetch that is still in flight when you call `clear()` schedules a new timer once it settles, so wait for pending fetches to settle first. diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 45c73916403..027c8b24c3a 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -596,9 +596,9 @@ const queryClient = new QueryClient({ }) ``` -Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you do need a finite `gcTime` on the server, a short one such as `2 * 1000` gives the app enough time to reference the data, while limiting how long each request stays in memory. +Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/functions/HydrationBoundary.md) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. Short finite values have the same risk: a prefetched query that finishes early can be removed once its `gcTime` passes, while a slower query is still being prefetched and before `dehydrate` runs. Keep `Infinity` on the server where you can. -If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. +If you set a finite `gcTime` on the server, call [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) once you no longer need the client and all of its queries have settled, for example after you have dehydrated or rendered with it. This removes every query from the cache and cancels its pending timer. A fetch that is still in flight when you call `clear()` schedules a new timer once it settles, so wait for pending fetches to settle first. ### Caveat for Next.js rewrites diff --git a/docs/framework/solid/guides/ssr.md b/docs/framework/solid/guides/ssr.md index aeef299ac9a..302c7275614 100644 --- a/docs/framework/solid/guides/ssr.md +++ b/docs/framework/solid/guides/ssr.md @@ -225,4 +225,4 @@ const queryClient = new QueryClient({ }) ``` -If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. +If you set a finite `gcTime` on the server, call [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) once the response has been sent and all of the client's queries have settled. This removes every query from the cache and cancels its pending timer. A fetch that is still in flight when you call `clear()` schedules a new timer once it settles, so wait for pending fetches to settle first. diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index 3f6ac84cb2f..c3826099192 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -313,7 +313,7 @@ const queryClient = new QueryClient({ }) ``` -If you set a finite `gcTime` on the server, add a call to [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) after the request is handled and dehydrated state has been sent to the client. This removes every query from the cache and cancels its pending timer. +If you set a finite `gcTime` on the server, call [`queryClient.clear()`](../reference/classes/QueryClient.md#clear) once you no longer need the client and all of its queries have settled, for example after you have dehydrated or rendered with it. This removes every query from the cache and cancels its pending timer. A fetch that is still in flight when you call `clear()` schedules a new timer once it settles, so wait for pending fetches to settle first. ## `dehydrate`/`hydrate` options