fix: nextjs high memory usage#5510
Conversation
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| JavaScript | Apr 20, 2026 12:13p.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
|
Thank you for the contribution. I assume you would like to participate in the bounty mentioned in #3759 ? |
Yes thank you. memory consumption on nextjs increased rapidly as refreshes were done against homarr, corresponding to an increase in CLOSE_WAIT connections - after 100 or so refreshes RSS was at 1.5GB - after the changes RSS stabilised around the 550MB Mark - and the CLOSE_WAIT cleanup script after 60 seconds only left 2 ESTABLISHED connections and zero CLOSE_WAIT. the gzip compression had the biggest effect and the rest of the changes focussed around removing stale CLOSE_WAIT connections in nextjs. |
9561685 to
c5e69c3
Compare
|
Not sure, but I don't see any improvement regarding the memory usage: old [NO TRAFFIC]:
new [NO TRAFFIC]:
Maybe I don't understand it correctly, but at least the base usage is the same |
|
I've published it under the image tag |
Base usage at startup should be the same, (my previous testing was locally on linux, not the docker image built) - the changes should affect memory growth over time, I would say run the ghcr.io/homarr-labs/homarr:kimpossible-potential-fix image for a few days and see how it handles things. |
d09e75c to
1e761d1
Compare
|
@manuel-rw I ran homarr for 72 hours and accessed from 2 different locations and kept the tabs open for websocket updates, everything seems very stable and no memory growth, you can actively see the GC reducing memory occasionally:
I'd consider this fixed, but outside testing would confirm. apologies for my messy committing and force-pushing, hopefully if this works it can be squash merged. |
|
Hi, I did have a look on the weekend. I think you may have misread the graphs that you have created. The total RSS is the total amount of RAM, allocated by all three processes. That amount is still mostly unchanged from the latest Homarr version. Your screenshot shows a peak of 1GiB with an average of 800MiB. Some of your changes may definitely help with some minor allocations and we might merge this, but sadly it doesn't qualify for our bounty aa far as I can tell. The bounty is for "significant reductions in memory consumption". @Meierschlumpf do you agree, that this doesn't qualify as "significant"? If you do manage to reduce usage significantly without breaking functionality, feel free to submit another PR (ideally separate from this) and we're happy to pay out the bounty! |
Addresses high memory usage by adding gzip compression to nginx (removing it from nextjs) socket cleanup handling, and tRPC client optimizations. Ref: homarr-labs#3759
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: homarr-labs#3759
…e CLOSE_WAITS from even appearing now: [10:20:20] rss=292.3MiB LISTEN=1 [10:20:25] rss=292.3MiB LISTEN=1 [10:20:31] rss=310.6MiB LISTEN=1 TIME_WAIT=118 [10:20:36] rss=336.7MiB LISTEN=1 TIME_WAIT=283 [10:20:41] rss=336.2MiB LISTEN=1 TIME_WAIT=453 [10:20:47] rss=370.5MiB LISTEN=1 TIME_WAIT=633 [10:20:52] rss=371.1MiB LISTEN=1 TIME_WAIT=804 [10:20:58] rss=396.4MiB LISTEN=1 TIME_WAIT=976 [10:21:03] rss=417.8MiB LISTEN=1 TIME_WAIT=1196 [10:21:08] rss=425.8MiB LISTEN=1 TIME_WAIT=1260 [10:21:14] rss=425.6MiB LISTEN=1 TIME_WAIT=1260 [10:21:19] rss=425.6MiB LISTEN=1 TIME_WAIT=1260 [10:21:24] rss=425.6MiB LISTEN=1 TIME_WAIT=1260 [10:21:30] rss=425.6MiB LISTEN=1 TIME_WAIT=1199 [10:21:35] rss=425.6MiB LISTEN=1 TIME_WAIT=1085 [10:21:41] rss=425.6MiB LISTEN=1 TIME_WAIT=970 [10:21:46] rss=425.6MiB LISTEN=1 TIME_WAIT=684 [10:21:51] rss=425.6MiB LISTEN=1 TIME_WAIT=520 [10:21:57] rss=288.0MiB LISTEN=1 TIME_WAIT=398 [10:22:02] rss=288.0MiB LISTEN=1 TIME_WAIT=113 [10:22:07] rss=288.0MiB LISTEN=1 [10:22:13] rss=288.0MiB LISTEN=1 [10:22:18] rss=288.0MiB LISTEN=1 no more CLOSE_WAIT sockets setting the v8 heap to 1GB will still allow gc to trigger, gently and then more aggressively as the heap usage gets close to 1GB. Even 512Mb would be fine...
|
Also, a quick recovery is expected for most frameworks unless there is a memory leak. But as @Meierschlumpf also posted above, the base usage seems unchanged or barely measureable. |
1e761d1 to
6fd69be
Compare
I think my fixes address unfettered memory growth, not base usage, in which case I would agree with your conclusion. Either way hopefully the fixes help overall. |
|
Yes, definitely. As promised, we will review this soon when we have time besides our job. |
| headers: createHeadersCallbackForSource("nextjs-react (form-data)"), | ||
| }), | ||
| false: httpBatchStreamLink({ | ||
| false: httpBatchLink({ |
There was a problem hiding this comment.
I'm not familiar enough with trpc.
@Meierschlumpf will the batching work here? Does it break anything?
https://tanstack.com/intent/registry/%40trpc__client/links#httpbatchlink----batch-multiple-calls-into-one-request
There was a problem hiding this comment.
Not sure, but I would imagine experience wise it is better to have stream link as then the results will stream in bit by bit (instead of it having to collect all data and return at the end)


Homarr
Thank you for your contribution. Please ensure that your pull request meets the following pull request:
pnpm build, autofix withpnpm format:fix)devbranchx,y,ior any abbrevation)Multiple issues idenfitied and tested locally:
vercel/next.js#89091 - The retainer chain was traced by a community member — when compression is enabled and a client disconnects mid-stream, the ServerResponse is held alive by AfterContext closures that reference the zlib Gzip instance. The socket never gets closed, sits in CLOSE-WAIT, and the native zlib buffers leak.
Setting compress: false eliminates that entire retention path. Multiple reporters on the issue confirmed it resolved their memory growth. The trade-off is that responses won't be gzip'd by Next.js. If you ever put this behind a reverse proxy (nginx), that handles compression instead.
vercel/next.js#89091
vercel/next.js#92287 - The httpBatchStreamLink vs httpBatchLink swap isn't tied to a single GitHub issue — it's the general recommendation from the tRPC community and multiple reporters on vercel/next.js#89091 and vercel/next.js#92287. The reasoning is straightforward: httpBatchStreamLink opens a long-lived streaming HTTP response that the client may abandon (page navigation, refresh), leaving the server-side socket in CLOSE-WAIT. httpBatchLink sends a single atomic response — the connection completes before the client can abandon it.
custom socket-cleanup-cjs to cleanup CLOSE_WAIT sockets after 60 seconds.
reproducible in original dev branch with constant refreshing of homarr homepage with multiple apps, rss feeds and plex integration: