From c5e69c3e40eb7ed2be9fec9c36b6101253fcf53b Mon Sep 17 00:00:00 2001 From: KimP0ssible-no Date: Wed, 15 Apr 2026 12:15:29 +0200 Subject: [PATCH] fix: memory leak fixes for nextjs. Addresses high memory usage by adding gzip compression to nginx (removing it from nextjs) socket cleanup handling, and tRPC client optimizations. Ref: https://github.com/homarr-labs/homarr/issues/3759 --- .gitignore | 4 ++- Dockerfile | 1 + apps/nextjs/next.config.ts | 1 + apps/nextjs/socket-cleanup.cjs | 27 +++++++++++++++++++ .../app/[locale]/_client-providers/trpc.tsx | 4 +-- nginx.conf | 6 +++++ scripts/run.sh | 2 +- 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 apps/nextjs/socket-cleanup.cjs diff --git a/.gitignore b/.gitignore index 43488df7a..358b1c22e 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,6 @@ en.d.json.ts # personal agent config AGENTS.md -.opencode \ No newline at end of file +.opencode +.claude/CLAUDE.md +.claude/settings.json diff --git a/Dockerfile b/Dockerfile index 3db197429..f019892a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,6 +52,7 @@ COPY --from=builder /app/packages/db/migrations ./db/migrations # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder /app/apps/nextjs/.next/standalone ./ +COPY apps/nextjs/socket-cleanup.cjs ./apps/nextjs/socket-cleanup.cjs COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public COPY scripts/run.sh ./run.sh diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index a3dabe151..68d694efd 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -19,6 +19,7 @@ const withNextIntl = createNextIntlPlugin({ const nextConfig: NextConfig = { output: "standalone", + compress: false, // Workaround for vercel/next.js#89091 - zlib retains ServerResponse after client disconnect, causing CLOSE-WAIT socket leak reactStrictMode: true, // react compiler breaks mantine-react-table, so disabled for now //reactCompiler: true, diff --git a/apps/nextjs/socket-cleanup.cjs b/apps/nextjs/socket-cleanup.cjs new file mode 100644 index 000000000..7d1310ce9 --- /dev/null +++ b/apps/nextjs/socket-cleanup.cjs @@ -0,0 +1,27 @@ +// Periodic cleanup of zombie CLOSE-WAIT sockets in Next.js standalone mode. +// Workaround for vercel/next.js#89091 — AfterContext retains ServerResponse +// after client disconnect, leaving sockets in CLOSE-WAIT indefinitely. +// +// Usage: node --require ./socket-cleanup.cjs server.js +// or: NODE_OPTIONS="--require /app/socket-cleanup.cjs" node server.js + +const INTERVAL_MS = 60_000; // Check every 60 seconds + +setInterval(() => { + let destroyed = 0; + for (const handle of process._getActiveHandles()) { + if ( + handle.constructor?.name === "Socket" && + handle.localPort === 3000 && + handle.readyState === "writeOnly" && + !handle._httpMessage && + handle.bytesWritten === 0 + ) { + handle.destroy(); + destroyed++; + } + } + if (destroyed > 0) { + console.log(`[socket-cleanup] Destroyed ${destroyed} zombie socket(s)`); + } +}, INTERVAL_MS).unref(); // unref() so this timer doesn't prevent process exit diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 0e36cb4a1..c75902efb 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -7,7 +7,7 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; import { createWSClient, - httpBatchStreamLink, + httpBatchLink, httpLink, isNonJsonSerializable, loggerLink, @@ -90,7 +90,7 @@ export function TRPCReactProvider(props: PropsWithChildren) { url: getTrpcUrl(), headers: createHeadersCallbackForSource("nextjs-react (form-data)"), }), - false: httpBatchStreamLink({ + false: httpBatchLink({ transformer: superjson, url: getTrpcUrl(), maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length diff --git a/nginx.conf b/nginx.conf index 4b82c8ca5..107bc6be1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -3,6 +3,12 @@ events { } http { + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; + gzip_min_length 1000; + gzip_proxied any; + gzip_vary on; + server { listen 7575; listen [::]:7575; diff --git a/scripts/run.sh b/scripts/run.sh index 4d90499d5..6aae28fcf 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -51,7 +51,7 @@ TASKS_PID=$! node apps/websocket/wssServer.cjs & WSS_PID=$! -node apps/nextjs/server.js & +node --require apps/nextjs/socket-cleanup.cjs apps/nextjs/server.js & NEXTJS_PID=$! # Function to handle SIGTERM and shut down services