Memory Problems with Tanstack Query and SSR
Which project does this relate to?
Start
Describe the bug
I have a kind of weird bug which is probably some misconfiguration on my side. When running my Tanstack Start app the memory usage grows over time until it exceeds the limit, crashes and then restarts. I did a heap snapshot and this is probably a issue with the queryClient.
I am not that used to bug/issue reporting. Please tell me which information you need and how I can make this as easy as possible for you
Your Example Website or App
https://github.com
Steps to Reproduce the Bug or Issue
I can try to provide a reproduction example if there is no initial dumb error in my code
Expected behavior
Memory should be more or less stable
Screenshots or Videos
I can provide a link to the heap snapshots if needed.
Platform
- Router / Start Version: 1.133.27
- OS: not relevant
- Browser: [e.g. Chrome, Safari, Firefox]
- Browser Version: [e.g. 91.1]
- Bundler: vite (deployed with nitro and coolify to a VPS
- Bundler Version: 7.0.6
Additional context
My Setup:
import { QueryClient } from '@tanstack/react-query';
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query';
import { queryClientConfig } from './context/tanstack-query-context';
import 'leaflet/dist/leaflet.css';
import { configureWebClient, getWebClient } from '@nono/api-client';
import * as Sentry from '@sentry/tanstackstart-react';
import { z } from 'zod';
import GeneralError from './features/errors/general-error';
import NotFoundError from './features/errors/not-found-error';
import { routeTree } from './routeTree.gen';
import { loadCookiesFn } from './utils/cookies';
z.config(z.locales.de());
configureWebClient({
baseUrl: import.meta.env.VITE_API_URL,
credentials: 'include',
});
// Global flag to prevent interceptor registration on every SSR request
let interceptorRegistered = false;
// Add cookies per-request (needed for SSR - server needs to forward cookies from incoming request)
// On the server, loadCookiesFn() reads from request headers; on client, it reads from browser cookies
if (!interceptorRegistered) {
getWebClient().interceptors.request.use(async (request) => {
const cookies = loadCookiesFn();
if (cookies) {
// Merge with existing Cookie header if present
const existingCookie = request.headers.get('Cookie');
if (existingCookie) {
request.headers.set('Cookie', `${existingCookie}; ${cookies}`);
} else {
request.headers.set('Cookie', cookies);
}
}
return request;
});
interceptorRegistered = true;
}
export function getRouter() {
const queryClient = new QueryClient(queryClientConfig);
const router = createTanStackRouter({
routeTree,
context: {
queryClient,
user: null,
session: null,
organizations: [],
avatarUrl: null,
},
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
defaultErrorComponent: () => <GeneralError />,
defaultNotFoundComponent: () => <NotFoundError />,
scrollRestoration: true,
});
if (!router.isServer) {
Sentry.init({
...
});
}
setupRouterSsrQueryIntegration({
router,
queryClient,
});
return router;
}
export const Route = createRootRouteWithContext<RouterContext>()({
validateSearch: z.object({
preview: z.boolean().optional(),
}),
beforeLoad: async ({ context }) => {
const session = await prefetchSession(context.queryClient);
return {
user: session.data?.user ?? null,
session: session.data?.session ?? null,
};
},
head: () => ({
...
// Detect if running on server (SSR) or client
const isServer = typeof window === 'undefined';
export const queryClientConfig: QueryClientConfig = {
defaultOptions: {
queries: {
// dehydrate: { serializeData: superjson.serialize },
// hydrate: { deserializeData: superjson.deserialize },
staleTime: STALE_TIME,
// Aggressive GC on server to prevent memory accumulation across SSR requests
// Client can keep longer cache for better UX
gcTime: isServer ? 1000 * 60 * 1 : 1000 * 60 * 10, // 1min SSR, 10min client
placeholderData: keepPreviousData,
retry: (failureCount, error) => {
if (failureCount >= 2) return false;
if (failureCount >= 0 && import.meta.env.DEV) return false;
if (error.message?.includes('401') || error.message?.includes('403'))
return false;
return true;
},
// refetchOnWindowFocus: import.meta.env.PROD,
// retry: 0,
refetchOnWindowFocus: false,
},
mutations: {
// Shorter GC time on server for mutations as well
gcTime: isServer ? 1000 * 60 * 1 : 1000 * 60 * 5, // 1min SSR, 5min client
onError: (error) => {
const schemaCheck = ApiResponseFailureSchema.safeParse(error);
if (schemaCheck.success) {
if (schemaCheck.data.error instanceof ZodError) {
toast.error(
z.prettifyError(schemaCheck.data.error) ??
'An unknown error occurred'
);
return;
}
toast.error(schemaCheck.data.error.title, {
description: schemaCheck.data.error.detail,
});
return;
}
toast.error(error.message ?? 'An unknown error occurred');
},
},
},
}
The way i use beforeLoad and loader (beforeLoad for guarding and loader for data prefetching)
export const Route = createFileRoute('/manage')({
component: RouteComponent,
beforeLoad: async ({ context }) => {
if (!context.user || !context.session) {
throw redirect({ to: '/auth/sign-in' });
}
const organizationId = context.session.activeOrganizationId;
if (!organizationId) {
throw redirect({ to: '/' });
}
// Pre-fetch organization and member in parallel
const [
organizationResponse,
organizationSettingsResponse,
venuesResponse,
eventsResponse,
] = await Promise.all([
context.queryClient.ensureQueryData(
getOrganizationOptions({
path: { organizationId },
})
),
context.queryClient.ensureQueryData(
getOrganizationSettingsOptions({
path: { organizationId },
})
),
context.queryClient.ensureQueryData(
listVenuesOptions({
query: { organizationId, pageIndex: 0, pageSize: 10 },
})
),
context.queryClient.ensureQueryData(
listEventsOptions({
query: { organizationId, pageIndex: 0, pageSize: 10 },
})
),
]);
const organization = {
...organizationResponse.data,
metadata: organizationResponse.data.metadata,
};
return {
user: context.user,
session: context.session,
organizationId,
organization,
organizationSettings: organizationSettingsResponse.data,
venues: venuesResponse.data.items,
events: eventsResponse.data.items,
logoUrl: organization.imageUrl ?? null,
mainEntry: {
label: organization.name || m.organization_info_title(),
href: '/manage/dashboard',
} as HeaderBreadcrumbItem,
};
},
loader: async () => {
return {
sidebarReferenceContext: {
type: 'organization',
id: null,
name: null,
} as SidebarReferenceContext,
};
},
});
export const Route = createFileRoute('/manage/venues/')({
component: RouteComponent,
validateSearch: zListVenuesData.shape.query
.omit({ organizationId: true })
.nonoptional(),
loaderDeps: ({ search }) => search,
loader: async ({ context, deps }) => {
await context.queryClient.ensureQueryData(
listVenuesOptions({
query: {
...deps,
organizationId: context.organizationId,
},
})
);
return {
organizationId: context.organizationId,
breadcrumb: [
{
label: m.venues_title(),
href: '/manage/venues',
},
] as HeaderBreadcrumbItem[],
};
},
head: ({ match }) => ({
meta: createMetadata({
title: m.venues_title(),
url: match.pathname.toString(),
}),
}),
});
๐ CodeRabbit Plan Mode
Generate an implementation plan and agent prompts for this issue.
- [ ] Create Plan
๐ Related PRs
TanStack/router#5215 - fix: various fixes [merged] TanStack/router#5363 - examples: Add a new TanStack Start example for typed readable streams [merged] TanStack/router#5665 - test(solid-start): fix solid-query when used in ssr [merged] TanStack/router#5837 - docs(solid-start): add start-streaming-data-from-server-functions example [merged] TanStack/router#5896 - fix: memory leaks [merged]
๐ค Suggested Assignees
- schiller-manuel
- fulopkovacs
- brenelz
- birkskyum
๐งช Issue enrichment is currently in early access.
To disable automatic issue enrichment, add the following to your .coderabbit.yaml:
issue_enrichment:
auto_enrich:
enabled: false
I found the Update fix:memory leaks. I will update and see if that resolves the issue
please let us know if the issue still persists. if yes, then please provide a complete reproducer project as a git repo.
what was the result of this?