Add --max-old-space-size=1024 to the node process running the Next.js server. This caps V8's old-generation heap at 1024MB, forcing the garbage collector to run more aggressively as memory usage approaches the limit. Previously, users saw Next.js RSS grow to 4GB+. This was caused by a reference chain that prevented GC from freeing memory: AfterContext (vercel/next.js#89091) retained ServerResponse objects after client disconnect, which held zlib Transform streams (from compress: true), which allocated native C++ buffers outside V8's heap entirely — invisible to GC and uncapped by --max-old-space-size. Combined with the prior commit's fixes (compress: false moves gzip to nginx, httpBatchLink closes connections faster, socket-cleanup.cjs destroys zombie CLOSE_WAIT sockets every 60s), GC can now actually reclaim memory because: - zlib native buffers no longer exist (compress: false) - The AfterContext → ServerResponse reference chain is broken by socket cleanup destroying the underlying sockets - The JS heap is capped at 1024MB so V8 GCs more frequently for the memory it does control Also fixes the Dockerfile to copy socket-cleanup.cjs from the builder stage instead of the host context. Ref: https://github.com/homarr-labs/homarr/issues/3759
27 lines
878 B
JavaScript
27 lines
878 B
JavaScript
// 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
|
|
|
|
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
|