Why next dev keeps eating memory, and what actually helps
You start next dev in the morning. By mid-afternoon the fan has been screaming for an
hour, saving a file takes eleven seconds, and the terminal finally gives up:
<--- Last few GCs --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
You restart it, get another three hours, and repeat. On a 16GB machine this is annoying. On the 8GB laptop most developers I work with in Nairobi actually own, it makes a large Next.js app close to unusable, because the dev server is competing with Chrome and a database container for the same memory.
The advice you will find is "restart the dev server periodically" and
"--max-old-space-size=8192". Neither is a fix. One is a coping strategy and the other
just moves the wall further away. What is worth your time is finding out which of the four
things that consume memory in a dev session is consuming yours, because the fix is completely
different in each case, and for at least one of them the answer is genuinely "this is expected, buy
more RAM or split the app".
there is no single Next.js memory leak to fix. Work through these in order:
- Measure first. Run
NODE_OPTIONS=--inspect next devand compare two heap snapshots taken twenty minutes apart in Chrome DevTools. - Cut module count. Barrel imports from icon and utility packages can pull
thousands of modules into the graph. Use
experimental.optimizePackageImportsor deep imports. - On webpack (Next 15, or Next 16 with
--webpack), setexperimental.webpackMemoryOptimizations: true. - Audit your own module-scope state for anything recreated on every hot reload: database clients, intervals, event listeners.
- Try Turbopack, which is the default in Next.js 16 and has a different memory profile entirely.
What this looks like before it crashes
Watch the resident set size of the dev process rather than waiting for the OOM:
watch -n 30 'ps -o pid,rss,etime,cmd -C node --sort=-rss | head -5'
The rss column is in kilobytes. What you are looking for is the shape of the curve,
not the number. A dev server that climbs quickly for the first few minutes as it compiles routes and
then plateaus is behaving correctly, even if the plateau is uncomfortably high. A dev server whose
RSS steps up after every save and never comes back down has something retaining old work.
Why this happens, and why there is no single fix
The Next.js dev server is not a web server that happens to compile. It is a compiler that never exits, holding a live module graph so it can rebuild the smallest possible slice when you save. Four separate things live in that process, and they fail in different ways.
The Next.js team has been open about this in the long-running development high memory usage issue, which is still open. Their consistent finding when a report comes with a runnable reproduction is that it is usually not one leak in the framework: they have pointed to application bugs, custom webpack configurations adding source maps, and single icon-library imports that drag in the order of eleven thousand modules. They have also said plainly that they cannot narrow reports down without code they can run. That is a fair position and it has a practical consequence for you: if you want this fixed, either you find your own layer 3 or 4 problem, or you produce a repro nobody else has managed to produce.
One more documented behaviour worth knowing: when the server starts it preloads each page's JavaScript modules into memory instead of doing it per request. That is a deliberate speed trade, and it means a very large app has a large baseline before you have made a single request.
Step 1: Measure before you change anything
NODE_OPTIONS=--inspect next dev
Open chrome://inspect in Chrome, click "inspect" on the Node target, go to the Memory
tab, and take a heap snapshot immediately after the first page compiles. Then work normally for
fifteen to twenty minutes, editing files you would normally edit, and take a second snapshot.
Switch the comparison dropdown to "Objects allocated between Snapshot 1 and Snapshot 2" and sort by retained size. You are looking for two patterns:
- Many copies of the same constructor where you expect one. Five instances of a database client, or forty of a class you only ever instantiate at module scope, is layer 4 and it is yours to fix.
- Large retained sizes under compiler internals with retainer chains that lead back to your own modules. That is layer 3, and the culprit is usually something in your code holding the reference.
If you are chasing build-time rather than dev-time memory, Next.js has a purpose-built flag:
next build --experimental-debug-memory-usage, available since 14.2, prints heap and
garbage collection statistics throughout the build and takes snapshots automatically as usage
approaches the limit. It is not compatible with the webpack build worker.
Step 2: Look at what you are importing
This is the highest-yield change in most codebases and it is not really a memory fix, it is a
module-count fix. An import like import { ChevronDown } from 'some-icon-pack' resolves
through the package's barrel file, and depending on the package the bundler may end up walking every
icon in it. Multiply by a few such packages and the module graph is an order of magnitude larger than
your app.
Two ways out. Import deeply, from the specific file, or let Next.js rewrite the barrel import for you:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
optimizePackageImports: ['some-icon-pack', 'some-ui-kit'],
},
}
export default nextConfig
Next.js already applies this optimisation to a list of well-known packages automatically; the config is for the ones it does not know about. Check your own dependencies before assuming you are covered.
Step 3: If you are on webpack, use the memory optimisation flag
Since v15.0.0 there is a documented, low-risk switch that changes webpack behaviour to reduce peak memory, at the cost of slightly slower compiles:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
webpackMemoryOptimizations: true,
},
}
export default nextConfig
Two related knobs, both documented: disabling source map generation with
productionBrowserSourceMaps: false and
experimental.serverSourceMaps: false, and setting
experimental.preloadEntriesOnStart: false to stop the server loading every route's
modules at boot. The second one trades a lower startup footprint for slower first hits, and the docs
are honest that once every page has been requested the footprint ends up in the same place.
Step 4: Audit your own module-scope state
This is layer 4, and in my experience it is the most common cause that is actually fixable. Every hot reload re-evaluates the modules you touched. Anything created at module scope is created again. If the old one is still referenced by something long-lived, it never goes away.
// Leaks across hot reloads: a new client on every reload of this module
export const db = new DatabaseClient(process.env.DATABASE_URL!)
// Survives hot reloads: one client per process in development
const globalForDb = globalThis as unknown as { db?: DatabaseClient }
export const db =
globalForDb.db ?? new DatabaseClient(process.env.DATABASE_URL!)
if (process.env.NODE_ENV !== 'production') globalForDb.db = db
The same pattern applies to anything with a lifecycle: connection pools, message queue consumers,
cache clients, setInterval at module scope, and listeners attached to
process or any other global. A dead giveaway that you have this problem is Node printing
a listener warning after a few edits:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 exit listeners added to [process]. Use emitter.setMaxListeners() to increase limit
That warning is not noise. It is telling you exactly how many times a module has been re-evaluated while the previous copies stayed alive. Fix the cause; do not raise the limit.
Step 5: Consider the bundler itself
Turbopack is stable and the default bundler for both next dev and
next build in Next.js 16, with --webpack available to opt out. If you are
on Next 15, next dev --turbopack is the equivalent switch.
This matters for memory in a way that is easy to miss: Turbopack does most of its work outside the
JavaScript heap, so the diagnostics in step 1 will show you far less, and raising
--max-old-space-size will not affect the part of the process that is actually growing.
Turbopack resolves the problem for some people and not for others, so treat it as a thing to measure
rather than a cure. What you must not do is carry over webpack-specific mitigations into a Turbopack
project and then wonder why nothing changed; webpackMemoryOptimizations and custom
webpack: config in next.config do nothing when webpack is not running.
Verification: prove the change did something
Guessing is what makes this problem eternal. Use a fixed, repeatable loop:
# log RSS in MB every 30 seconds to a file
while true; do
echo "$(date +%H:%M:%S) $(ps -o rss= -C node --sort=-rss | head -1 | awk '{print $1/1024}')" \
>> /tmp/nextdev-rss.log
sleep 30
done
Then run the same twenty-minute routine before and after your change: load the same three routes, edit the same two components ten times each, and compare the two logs. You want a curve that flattens, not a lower starting point. A change that lowers the baseline but keeps the same upward slope has bought you an hour, not fixed anything.
What people get wrong
"Just raise --max-old-space-size." It raises the ceiling before V8
gives up. On a genuinely large monorepo that is a legitimate stopgap, and I have shipped it in a
package.json script more than once. But if memory grows without bound, a bigger heap
means a longer wait for the same crash and worse garbage collection pauses on the way there. Use it
knowingly, and never as the end of the investigation.
"Delete .next and it will be fine." Clearing the build directory
fixes stale artefacts and corrupted caches. It has nothing to do with what a running process retains
in memory.
Copying the "disable webpack cache" snippet without reading it. The example in the
Next.js docs is guarded with if (config.cache && !dev). It is aimed at builds.
Disabling the cache in development makes every compile slow, which is the thing the dev server exists
to avoid.
"It is a framework leak, so there is nothing I can do." The issue is real and still open, and I am not going to pretend otherwise. But the reports that got traced to a cause mostly landed on module count, custom bundler config, or application code, and every one of those is in your control. Assuming it is somebody else's bug is how a team spends two years restarting a dev server.
When it is still growing
- Check where your files live. Running a project from a Windows drive under WSL2
(anything under
/mnt/c), from a network share, or from a synced cloud folder pushes the file watcher into polling mode, which is dramatically more expensive. Move the repository into the Linux filesystem and re-measure. - Reduce the surface you are compiling. In a large monorepo, run the dev server for one app rather than all of them, and avoid touching files that invalidate half the graph. This is not elegant, but a 300-module dev session behaves very differently from a 30,000-module one.
- Bisect your dependencies. Comment out a suspicious provider or analytics wrapper at the root layout and re-run the twenty-minute loop. Root-level wrappers are load-bearing for the entire module graph.
- If you have a genuine reproduction, publish it. A minimal repository that grows without bound is worth more to this problem than a thousand "same here" comments, and it is the one thing the maintainers have repeatedly said they need.
If your dev server is slow rather than fat, that is a different investigation, and hydration or caching behaviour is usually the culprit: see finding the real hydration mismatch and why revalidate is ignored in production.
Frequently asked questions
- Does Next.js have a memory leak in the dev server?
- There is a long-running, still-open issue about high memory usage in development, and the Next.js team has confirmed it exists. When individual reports have been traced to a cause, they have mostly turned out to be module count from barrel imports, custom webpack configuration, or application code retaining state across hot reloads, rather than a single leak in the framework. Measuring your own process is the only way to know which one you have.
- Does raising --max-old-space-size fix the Next.js dev server memory problem?
- No, it raises the heap limit before V8 aborts, which delays the crash rather than preventing it and makes garbage collection pauses longer. It is a reasonable stopgap on a very large monorepo, but if memory grows without bound you will hit the new ceiling too. It also has no effect on Turbopack, which does most of its work outside the JavaScript heap.
- How do I find what is leaking in my Next.js dev server?
- Start the server with NODE_OPTIONS=--inspect next dev, open chrome://inspect, and take a heap snapshot right after the first compile. Work normally for fifteen to twenty minutes, take a second snapshot, then compare objects allocated between the two and sort by retained size. Multiple instances of something you create once at module scope, such as a database client, points at code that is recreated on every hot reload.
- Will switching to Turbopack fix high memory usage in next dev?
- Sometimes. Turbopack is stable and the default bundler in Next.js 16 for both dev and build, and it has a completely different memory profile from webpack, so it resolves the problem for some projects and not others. Treat it as something to measure rather than a guaranteed cure, and remember that webpack-specific mitigations such as experimental.webpackMemoryOptimizations do nothing once webpack is not the bundler.