Skip to content

fix: nextjs high memory usage#5510

Open
KimP0ssible-no wants to merge 4 commits into
homarr-labs:devfrom
KimP0ssible-no:fix/high_memory_usage
Open

fix: nextjs high memory usage#5510
KimP0ssible-no wants to merge 4 commits into
homarr-labs:devfrom
KimP0ssible-no:fix/high_memory_usage

Conversation

@KimP0ssible-no
Copy link
Copy Markdown

@KimP0ssible-no KimP0ssible-no commented Apr 14, 2026


Homarr

Thank you for your contribution. Please ensure that your pull request meets the following pull request:

  • Builds without warnings or errors (pnpm build, autofix with pnpm format:fix)
  • Pull request targets dev branch
  • Commits follow the conventional commits guideline
  • No shorthand variable names are used (eg. x, y, i or any abbrevation)
  • Documentation is up to date. Create a pull request here.

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:

ss -tnp | grep ":3000" | awk '{print $1}' | sort | uniq -c | sort -rn # check for connection states
ps aux | grep -E 'tasks\.cjs|wssServer|next-server' | grep -v grep #check RSS for homarr apps

@KimP0ssible-no KimP0ssible-no requested a review from a team as a code owner April 14, 2026 17:06
@deepsource-io
Copy link
Copy Markdown
Contributor

deepsource-io Bot commented Apr 14, 2026

DeepSource Code Review

We reviewed changes in 841fdbd...6fd69be on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

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.

@KimP0ssible-no KimP0ssible-no changed the title https://github.com/homarr-labs/homarr/issues/3759 fix: nextjs high memory usage Apr 14, 2026
@manuel-rw
Copy link
Copy Markdown
Member

Thank you for the contribution. I assume you would like to participate in the bounty mentioned in #3759 ?
Can you outline how memory consumption has changed after this change? We'll review this within the next days.

@KimP0ssible-no
Copy link
Copy Markdown
Author

Thank you for the contribution. I assume you would like to participate in the bounty mentioned in #3759 ? Can you outline how memory consumption has changed after this change? We'll review this within the next days.

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.

@KimP0ssible-no KimP0ssible-no force-pushed the fix/high_memory_usage branch 2 times, most recently from 9561685 to c5e69c3 Compare April 15, 2026 10:17
@Meierschlumpf
Copy link
Copy Markdown
Member

Not sure, but I don't see any improvement regarding the memory usage:

old [NO TRAFFIC]:

  • 685
  • 555
  • 542
  • 544
  • 544
  • 544
  • 562

new [NO TRAFFIC]:

  • 636
  • 558
  • 560
  • 568
  • 575
  • 579

Maybe I don't understand it correctly, but at least the base usage is the same

@Meierschlumpf
Copy link
Copy Markdown
Member

I've published it under the image tag kimpossible-potential-fix if you want to test it yourself (I had to fix the require in Dockerfile)

@KimP0ssible-no
Copy link
Copy Markdown
Author

kimpossible-potential-fix

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.

@KimP0ssible-no
Copy link
Copy Markdown
Author

kimpossible-potential-fix

Running the docker image, just ran a quick test by spamming the F5 refresh on homarr, and you can see the spike in memory usage from next-server, but a quick recovery/gc:

homarr_process_memory

I'll run a 24 hour test to check stability further.

@KimP0ssible-no KimP0ssible-no force-pushed the fix/high_memory_usage branch from d09e75c to 1e761d1 Compare April 17, 2026 08:40
@KimP0ssible-no
Copy link
Copy Markdown
Author

KimP0ssible-no commented Apr 20, 2026

@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:

homarr_process_memory

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.

@manuel-rw
Copy link
Copy Markdown
Member

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...
@manuel-rw
Copy link
Copy Markdown
Member

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.

@KimP0ssible-no KimP0ssible-no force-pushed the fix/high_memory_usage branch from 1e761d1 to 6fd69be Compare April 20, 2026 12:13
@KimP0ssible-no
Copy link
Copy Markdown
Author

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!

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.

@manuel-rw
Copy link
Copy Markdown
Member

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({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@manuel-rw manuel-rw requested a review from Meierschlumpf April 26, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants