Shortcut: paste your failing output into the free Next.js Build Error Decoder — it recognizes the heap-out-of-memory signature (and 12 others) and links straight to the fix that applies.
The message
You are running next dev (or a Node production server), the terminal prints
this, and the server restarts:
The server is running out of memory, restarting to free up memory
Routes reload slowly, sessions drop, and in dev your HMR stops working for a
few seconds every time it fires. This was filed as
vercel/next.js#46756 (dev
App Router + RSC accumulation) and tracked as NEXT-1353, with the production
side in vercel/next.js#49929
(NEXT-1314, high memory in deployed projects).
The message is a symptom, not a diagnosis. Next.js notices the Node heap
crossed its limit and restarts to recover. Your job is to find out why it
crossed — and there are only three possible buckets.
The three causes
- Dev-mode accumulation — the classic App Router + RSC hot-reload leak in Next.js 13.x. Repeated refreshes grow the heap until it restarts. This is a tooling problem, not your code.
-
A real leak in your app or deps — an in-memory cache without eviction,
event listeners never removed, a closure holding a growing dataset, a
Mapthat never gets pruned. -
A legitimate heap that is too big for the limit — a route that loads a
large dataset into memory at build time, a heavy
getStaticProps/generateStaticParamsjob, or sourcemaps in production.
You diagnose by bucket. The fixes are different.
First, stop the restarts (band-aid)
Raise the V8 heap limit so you can actually work while you investigate:
# 4 GB heap (dev)
NODE_OPTIONS=--max-old-space-size=4096 npm run dev
# 8 GB heap (large production builds)
NODE_OPTIONS=--max-old-space-size=8192 npm run build
This is not a fix. It is the room you need to take a heap snapshot instead of
getting restarted out of the inspector. Set it, then move to diagnosis.
If it only happens in dev — switch to Turbopack
The 13.x dev-mode leak in webpack's App Router pipeline is the single most
common cause of this message. If you are on Next.js 14+ and still hitting it
in dev, try Turbopack first:
next dev --turbo
Turbopack's module graph does not accumulate on hot reload the way webpack
did, so for the classic dev leak it resolves immediately. This is the lowest-
effort, highest-hit fix in this whole article.
If you are on an older 13.x and cannot move to Turbopack, upgrade to the
latest stable — the dev accumulation was steadily reduced across 13.x and
largely resolved in 14+. Do not stay on a leaking canary.
Diagnose a real leak with a heap snapshot
If Turbopack did not help, or the leak is in production, take a heap snapshot.
Run the dev server under the inspector:
NODE_OPTIONS='--inspect' npm run dev
# or for a production Node server:
NODE_OPTIONS='--inspect' node server.js
Open chrome://inspect → inspect → Memory tab → Heap snapshot.
Use the three-snapshot technique:
- Snapshot A — after the app warms up.
- Snapshot B — after running the leaking flow once.
- Snapshot C — after running it again.
In the comparison view (C vs B vs A), objects that grow between every snapshot
are your leak. Filter to "retained size" and look for your own domain objects
(users, rows, requests) — if you see your own types growing, the leak is in
your code. If you only see framework / node internals, the leak is upstream
(or you are holding references to your data in a structure you forgot about).
In server.js, log the heap on an interval to catch growth across requests:
// server.js (or a small instrumentation file)
setInterval(() => {
const m = process.memoryUsage();
console.log(
`rss=${(m.rss / 1048576).toFixed(0)}MB ` +
`heapUsed=${(m.heapUsed / 1048576).toFixed(0)}MB ` +
`heapTotal=${(m.heapTotal / 1048576).toFixed(0)}MB ` +
`external=${(m.external / 1048576).toFixed(0)}MB`,
);
}, 10_000);
A flat line means no leak. A steady climb that never settles means a leak.
Common app-level leaks
The usual suspects, in order of how often I have seen them:
-
Module-level
Map/Set/ array caches. Aconst cache = new Map()at module scope that you write to on every request and never evict from. Fix: bound it (an LRU), move it to a real cache (Redis / KV), or recompute. -
Event listeners never removed. A
setIntervalor an event emitter you attach on every request and neverremoveListener/clearInterval. Each request leaks the listener and everything it closes over. - Closures holding large datasets. A request handler that builds a giant array inside a closure passed to a long-lived callback. The array lives as long as the callback does.
-
Sourcemaps in production.
productionBrowserSourceMaps: trueor a misconfigured build keeps large source content in memory. Disable for the Node server, keep them only for client error reporting. -
Heavy
generateStaticParams/ build-time fetches. Pulling a large dataset into memory at build time to enumerate pages. Stream it from disk or a DB cursor instead.
Production levers
For deployed Next.js (Node server or serverless), memory is a resource you
budget for. The levers, biggest first:
-
Move large in-memory caches out of the process. Anything that grows
with traffic belongs in Redis, Upstash, or your platform's KV, not in a
Map. This is the single largest production win. -
Stream large responses. Do not
awaitan entire dataset into memory thenJSON.stringifyit. Stream it.Responsewith aReadableStream, or thestreamingrender APIs, keep the working set small. -
Prune dependencies.
next buildshows the largest server modules. A heavy dep loaded into the server bundle sits in memory for the life of the process. Lazy-import the rare ones withawait import(). - Disable sourcemaps on the server. They are for dev and error reporting; shipping them on the server inflates resident memory.
- Raise the serverless memory budget. On Vercel, serverless functions have a per-function memory ceiling (1 GB on Hobby, more on Pro — confirm on your plan). A route that genuinely needs more memory should be split out and given its own configuration rather than dragging the whole app up.
- Use the Edge Runtime for stateless routes. Edge functions have a smaller, fixed memory profile and no Node heap growth; move the routes that are pure request/response work over. For the gotchas of what you cannot run on Edge (no native TCP, no sync FS), the dynamic server usage / could not be rendered statically fix covers the boundary.
The dev-vs-prod split
A leak that only reproduces in next dev and never in next build / next is almost always the webpack dev pipeline. Do not spend two days
start
hunting a leak in your code that is actually webpack's hot-reload graph —
try Turbopack for thirty seconds first.
A leak that reproduces in next start against a production build is your
code or your deps. Take the three-snapshot path above.
And the failure mode that is not a leak at all: the build passes locally, you
deploy, and it breaks in production for a different reason. That is the
build passes locally, broken in production
postmortem, not this one. They look similar from the outside and have
completely different causes.
Production checklist
- [ ]
NODE_OPTIONS=--max-old-space-sizeset for the build and the server. - [ ] No module-level unbounded caches; every cache has an eviction policy or lives outside the process.
- [ ] No
setInterval/ listeners without a matching teardown. - [ ] Sourcemaps disabled on the Node server.
- [ ] Large routes streamed, not buffered.
- [ ] Heavy routes split out with their own memory budget if on serverless.
- [ ] Stateless routes on Edge where possible.
- [ ] An alert on
process.memoryUsage().heapUsedso you see growth before the restart message does.
For the broader performance posture — what to measure, what to move to Edge,
how to keep the bundle thin — the
Next.js performance optimization guide
is the longer read. For the deploy-time failure modes that look like memory
issues but are not (middleware not running, static assets 404), see
middleware not running in Vercel production
and the deploying Next.js + Supabase to production
guide.
TL;DR
- Restarting on memory? Raise the heap with
NODE_OPTIONS=--max-old-space-size=4096to stop the bleeding. - Only in dev? Switch to
next dev --turbo— the classic 13.x webpack leak is the most common cause and Turbopack sidesteps it. - In production? Take three heap snapshots, find what grows, move caches out of the process, stream large responses, disable server sourcemaps.
Related Articles
- Next.js Build Passes Locally, Broken in Production
- Next.js Performance Optimization Guide
- Fix "Dynamic Server Usage: Could Not Be Rendered Statically"
- Next.js Middleware Not Running in Vercel Production
- Deploying Next.js + Supabase to Production
- Fix Next.js Hydration Mismatch in App Router
- Fix Missing Suspense Boundary with useSearchParams
Originally published at https://www.iloveblogs.blog
Top comments (0)