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
This commit is contained in:
KimP0ssible-no committed 2026-04-15 12:15:29 +02:00
1 parent 6a45645354
commit c5e69c3e40
7 files changed
+41 -4

No files matched your search

+3 -1
View File
@@ -71,4 +71,6 @@ en.d.json.ts
# personal agent config
AGENTS.md
.opencode
.opencode
.claude/CLAUDE.md
.claude/settings.json
+1
View File
@@ -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
+1
View File
@@ -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,
+27
View File
@@ -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
@@ -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
+6
View File
@@ -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;
+1 -1
View File
@@ -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