Originally published at - the route-resolution cache's size function adds up the value strings but never the URL key, so it can retain ~1M keys (hundreds of MB) while believing it is tiny. Signature: slow heap drift over days that tracks unique URLs served (bot crawls, long slugs), not request volume; heap-snapshot retainer traces end at LRUNode. Tip: chart your heap against distinct routes, not requests per second.
Leak 2: the RSC render tree is retained on client aborts
- the sandbox TimeoutsManager only releases a timeout id on explicit clearTimeout. A one-line workaround that works today:
const timeoutId = setTimeout(() => {
fireAndForgetWork();
clearTimeout(timeoutId); // releases the id from the sandbox manager
}, 0);
Which one are you hitting?
| Growth shape | Correlates with | Suspect |
|---|---|---|
| Slow, monotonic drift over days | Unique URLs (bots, long slugs) | Router LRU cache (#94890) |
| Proportional to traffic; worse on heavy pages | RSC payload and client aborts | RSC tree retention (#94919) |
| Steps the GC never recovers | Requests through middleware | Sandbox timeouts (#95094) |
| No OOM, but sporadic 504s on serverless | Render cost, not memory | Your render is expensive |
To confirm: start with NODE_OPTIONS='--inspect' next start, take two heap snapshots separated by load, and read the retainers in Chrome DevTools' Memory tab.
On serverless the leak wears a disguise
You don't get the OOM - you get 504s. My blog's tag pages were timing out with FUNCTION_INVOCATION_TIMEOUT because of an O(N^2) post loader (a ~32-second render against a ~10-second limit); instance recycling had been hiding the waste. The fix was a module-level cache: memory spent on purpose, bounded and static per deploy.
The full diagnosis flow - which retainer maps to which issue, the exact docs commands, and my 504 postmortem with before/after numbers - is in the original post: https://xabierlameiro.com/blog/nextjs/nextjs-memory-leak-in-production
SOCIAL SHARE CARD GENERATOR