</>CodeWithKarani

Why next dev keeps eating memory, and what actually helps

Karani GeoffreyKarani Geoffrey9 min read

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 dev and 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.optimizePackageImports or deep imports.
  • On webpack (Next 15, or Next 16 with --webpack), set experimental.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.

Where the memory in a dev session actually sits 1. The module graph Every module your routes import, held in memory. Grows with dependency count, not with time. 2. The bundler cache Deliberate: it is what makes the second compile fast. High plateau is normal, not a leak. 3. Old module versions retained across hot reloads Anything still referencing the previous version keeps it alive. This is the one that climbs forever. 4. Your own module-scope state DB clients, intervals and listeners created at import time, recreated on every reload.
Layers 1 and 2 explain a high plateau. Only layers 3 and 4 explain a line that keeps going up, and layer 4 is the one you can actually fix today.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
#Next.js#Node.js#Webpack#Turbopack#Performance
Keep reading

Related articles