DEV Community

Cover image for Postmortem: The Next.js App Router Memory Leak of 2023
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Postmortem: The Next.js App Router Memory Leak of 2023

Issue #49929 on vercel/next.js opened on 2023-05-17 with a small reproduction: a Next.js 13.4.3-canary App Router project that behaved normally on Vercel and climbed to 220MB of steady memory use on Fly.io. It closed 2023-08-24 with 131 comments, screenshots from Kubernetes pods, Railway deployments, and Docker containers all showing the same shape — memory climbing until the process was killed and restarted — and two Vercel engineers, Tim Neutkens and Lee Robinson, running the investigation live in the thread. This is what the thread actually established, sourced from the issue itself, not from memory of "the App Router memory bug."

The symptom, stated precisely

Every report in the thread described the same pattern: a Next.js app using the App Router (introduced as the default in 13.4) would start at a normal baseline, then grow memory usage with every request or page navigation, and never release it — until the host's memory limit was hit and the process was killed. On free-tier hosts with 256MB of RAM (Fly.io's free allowance was cited repeatedly), this happened within hours. On larger Kubernetes pods with 2-4GB, it took days, but the trend line was the same: up, never down.

The scale of the reports made this hard to dismiss as one team's misconfiguration: karl-run measured 10x the idle memory of an equivalent Pages Router app on the same Kubernetes cluster; stx-chris watched a container "accumulate objects but never release them" until it hit its 2GB limit and restarted.

Why it took three months to find

Tim Neutkens' comments in the thread are the most useful part of it, because they explain why a memory leak in a framework used by millions of deployments is genuinely hard to diagnose — not because the team was slow, but because of what memory metrics actually measure:

"We used valgrind and lldb and found that the increase in memory usage is based on what the max-old-space-size and other memory options are set on Node.js. [...] there is about 50% of empty memory assigned regardless of it being used, we're assuming this is related to V8 pre-allocating a certain amount of memory to cover increases in heap usage."

In other words, a rising Resident Set Size (RSS) graph — which is what every screenshot in the thread showed — does not by itself prove a leak. V8 retains memory after a load spike instead of returning it to the OS immediately, which looks identical to a leak on a monitoring dashboard. This is the core reason the investigation dragged: most reports were RSS screenshots from production dashboards, and the team needed heap snapshots and a runnable reproduction to tell an actual leak apart from expected V8 behavior. timneutkens said as much directly, repeatedly asking commenters for a reproduction rather than another chart, and getting mostly charts back.

The two causes that were actually confirmed

Buried in the noise, two concrete, fixed causes emerged — and they are unrelated to each other, which is why no single patch "fixed the memory leak":

1. A real Node.js leak in undici's fetch(). Next.js calls fetch() internally with an AbortController signal. Below Node.js 18.17.0, this leaked memory on every call — a bug in Node's bundled undici (its fetch implementation), fixed in undici PR #2049, landing in Node.js 18.17.0. broksonic21's minimal reproduction confirmed this exactly: on Node 18.17.0 with the corresponding Next.js canary, the leak was gone; below that Node version, it wasn't.

2. The App Router's process-per-request-type overhead. Separately from the leak, App Router deployments started with more baseline processes than the Pages Router (one each for routing, App Router rendering, and Pages Router rendering — even when a project used only one router). Neutkens landed PR #53523 to remove one process, then PR #54143 to bring the count down to two, directly reducing the memory floor every App Router deployment started from — independent of whether the undici leak was present.

A third, popular workaround in the thread — uninstalling sharp or setting images: { unoptimized: true } — correlated with lower memory for several commenters, but the team never confirmed it as the same bug. Instead they opened a dedicated issue, #54482, to investigate Image Optimization's memory profile on its own. Conflating that workaround with "the fix" for this issue would be the wrong takeaway — it addressed a different, still-open question.

What actually shipped, and when

  • Node.js ≥ 18.17.0 — required, for the underlying undici/fetch() leak fix. Projects on older Node 18.x will still see it regardless of Next.js version.
  • Next.js 13.4.13-canary.12+ — fixed a related "zombie process" bug where next start kept running after Ctrl+C.
  • Next.js post-13.4.20 canaries (PRs #53523, #54143) — reduced the App Router's baseline process count, lowering the memory floor.

If your project is still pinned below 13.4.10 for this exact reason, both the Node.js upgrade and a Next.js upgrade past this window are required together — one without the other only removes part of the symptom.

Is this still relevant if you're starting a project today?

Directly, no — a project scaffolded with a current Next.js version already ships past every fix in this thread, and Node.js 18.17.0 is over three years old at this point. Indirectly, yes, in three situations that still show up in support threads:

  1. A package.json pinned during this window. Projects that pinned "next": "13.4.x" in mid-2023 to dodge this exact bug, then never revisited the pin, are still running code from before both fixes landed. npm ls next and compare against 13.4.20 as the floor.
  2. A base Docker image with an old Node.js. node:18-slim from before mid-2023 predates 18.17.0. If your Dockerfile pins a Node major without a minor/patch floor, you can rebuild on a fixed Next.js version and still carry the Node-side leak.
  3. Reading old Stack Overflow answers as current advice. Several highly-upvoted answers from 2023 recommend appDir: false or uninstalling sharp as the fix. Per this thread, appDir: false was a workaround for the process-count overhead specifically (moot after PR #54143 shipped), and the sharp correlation was never confirmed as the same root cause — treating either as a permanent fix for a 2026 App Router project misattributes the real cause.

The fastest check for an existing project: confirm node --version reports 18.17.0 or later, and next --version reports past 13.4.20 — if both hold, this specific thread does not apply to you, regardless of what a memory graph looks like.

Related Incidents


Originally published at https://www.iloveblogs.blog

Top comments (0)