Skip to content

Commit a51f2e8

Browse files
fix(web): create the QueryClient per-request on the server (#1575)
* fix(web): create the QueryClient per-request on the server The QueryClient was created at module scope, so on the server one instance — rooted for the process lifetime — was shared by every SSR render. Each render deposits query state into its cache under request-derived keys (repos, file paths, searches), and queries never become inactive server-side because unmount/unsubscribe never run during SSR, so gcTime eviction never fires. The cache anchored entire server render graphs (react-query observers, hook state, CodeMirror editor state) at ~400 MiB/h of genuine retention, measured in production by forcing a full GC via the inspector: 2073 -> 1826 MiB freed only 247 MiB at a 2 GiB heap. Follow TanStack Query's advanced SSR guidance: a fresh client per server render (it dies with the request, releasing everything anchored to it) and a module-scope singleton in the browser, deliberately not stored in React state so it survives React suspending during the initial render. This also removes a correctness defect the docs call out for module-scope clients: one request's cached query data could previously be rendered into another request's SSR HTML. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add changelog entry for query client leak fix Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5e13dbb commit a51f2e8

2 files changed

Lines changed: 43 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Fixed the web process being capped at a ~4GiB heap regardless of how much memory the container has, which caused multi-second garbage collection pauses on larger deployments. [#1569](https://github.com/sourcebot-dev/sourcebot/pull/1569)
1717
- Upgraded `@sentry/*` to `^10.70.0`, fixing memory leaks where spans retained request data indefinitely. [#1572](https://github.com/sourcebot-dev/sourcebot/pull/1572)
1818
- Fixed code search result links occasionally getting stuck during navigation and restored Cmd/Ctrl-click to open matches in preview. [#1574](https://github.com/sourcebot-dev/sourcebot/pull/1574)
19+
- Fixed a server-side memory leak where a single shared react-query cache retained state from every server render; the cache is now created per-request. [#1575](https://github.com/sourcebot-dev/sourcebot/pull/1575)
1920

2021
## [5.1.6] - 2026-08-10
2122

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
11
'use client';
22

33
import * as React from "react"
4-
import { QueryClient, QueryClientProvider as QueryClientProviderBase, QueryClientProviderProps } from "@tanstack/react-query"
4+
import { isServer, QueryClient, QueryClientProvider as QueryClientProviderBase, QueryClientProviderProps } from "@tanstack/react-query"
5+
6+
const makeQueryClient = () => {
7+
return new QueryClient();
8+
}
9+
10+
let browserQueryClient: QueryClient | undefined = undefined;
11+
12+
/**
13+
* On the server, always create a fresh QueryClient so it lives and dies with
14+
* the request. A module-scoped client is rooted for the process lifetime and
15+
* every SSR render deposits query state into it — with request-derived query
16+
* keys (repos, file paths, searches) that cache only ever grows, and queries
17+
* never become inactive server-side (unmount/unsubscribe don't run during
18+
* SSR), so nothing is ever evicted. In production this retained whole render
19+
* graphs at ~400 MiB/h until the container was OOM-adjacent and liveness
20+
* probes killed it.
21+
*
22+
* In the browser a singleton is correct (one user, unmounts run, gcTime
23+
* evicts) and deliberately NOT stored in React state, so the client survives
24+
* React suspending during the initial render.
25+
*
26+
* @see https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr
27+
*/
28+
const getQueryClient = () => {
29+
if (isServer) {
30+
return makeQueryClient();
31+
}
32+
33+
if (!browserQueryClient) {
34+
browserQueryClient = makeQueryClient();
35+
}
36+
return browserQueryClient;
37+
}
538

6-
const queryClient = new QueryClient();
7-
839
export const QueryClientProvider = ({ children, ...props }: Omit<QueryClientProviderProps, 'client'>) => {
9-
return (
10-
<QueryClientProviderBase client={queryClient} {...props}>
11-
{children}
12-
</QueryClientProviderBase>
13-
)
14-
}
40+
const queryClient = getQueryClient();
41+
42+
return (
43+
<QueryClientProviderBase client={queryClient} {...props}>
44+
{children}
45+
</QueryClientProviderBase>
46+
)
47+
}

0 commit comments

Comments
 (0)