Exit code 137 has not gone anywhere. What has changed is everything around it.

You run nuxt build on your $6-a-month VPS. The logs scroll. CPU climbs. Memory climbs. Then nothing. The process exits with code 137 and the server goes quiet.
You search the error. The top result is a thread from 2021. You follow the suggestion, bump the Node.js heap limit and the build completes. Three deploys later, you add a few new pages and it crashes again.
This problem has not been solved in five years of framework iteration. If anything, the gap between what modern JavaScript build tooling needs and what budget infrastructure provides has widened as frameworks like Nuxt 3, Next.js, SvelteKit and Astro all moved toward doing more analysis work at build time. The average developer is still deploying to the same 1GB VPS that served them perfectly fine with a simpler stack.
This article uses Nuxt as its primary example because the build memory problem is particularly well-documented there, but every fix and concept here applies equally to Next.js, SvelteKit, Astro, Remix and any other framework that runs a heavy Node.js build process. The underlying cause and the solution are the same across all of them.
This article explains why this keeps happening, what the correct fixes look like in 2026 (some of the advice floating around is outdated) and when you should stop patching and do something different.
What exit code 137 actually means
Exit code 137 is not a Node.js error. It is a Linux kernel signal.
When a process exceeds available memory, the Linux OOM (Out of Memory) killer sends signal 9 to that process. Signal 9 is SIGKILL. It cannot be caught or ignored. The process terminates immediately. Exit code = 128 + signal number = 128 + 9 = 137.
This matters because the failure is not always visible before it happens. Sometimes Node.js detects it is about to exhaust its V8 heap and prints:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memorySometimes the OS kills the process before Node.js gets to print anything. You just see the process exit with 137 and no explanation in your logs.
Both cases have the same root cause: the build process needed more memory than the server had available.
Why modern framework builds use more memory than they used to
The Nuxt 2 to Nuxt 3 migration is a useful case study because it is well-documented, but the pattern repeats across the ecosystem. Next.js, SvelteKit and Astro all went through similar architectural shifts that traded build-time memory for better output. Nuxt’s move from webpack to Vite for development and Nitro with Rollup for production builds is the specific path here. The developer experience improvement is real. Hot module replacement is dramatically faster. But production build memory usage did not decrease. For most projects it increased.
During nuxt build, several things happen in rapid sequence:
- TypeScript type checking across the entire codebase
- Vite module graph analysis for the client bundle
- Rollup tree-shaking and code splitting for the server-side bundle
- Nitro server bundle compilation
- Route-level code splitting with chunk optimisation for all detected routes
Nuxt 2 webpack builds were already memory-hungry but somewhat predictable. Nuxt 3 adds full TypeScript inference passes and runs more concurrent analysis. GitHub issue #26798 in the nuxt/nuxt repository (still active, with comments from 2025) documents builds consuming over 3.8GB of RAM on large Nuxt 3 projects.
The problem is not a flaw in Nuxt 3’s design. The analysis work it does produces better output. The problem is that Netlify’s default build container, Railway’s starter plan, DigitalOcean’s $6 Droplet and most budget VPS instances allocate 512MB to 1GB for build processes. That ceiling is well under what a mid-size Nuxt 3 project needs at peak build time.
Fix 1: increase the Node.js V8 heap limit
Node.js manages memory through the V8 engine’s heap allocator. On 64-bit systems running Node.js 20 or 22, the default heap ceiling is roughly 2GB. This limit exists to leave room for the OS and other processes: it is a ceiling, not a reservation.
You can raise it with the --max-old-space-size flag. The value is in megabytes.
The current recommended approach uses the NODE_OPTIONS environment variable:
NODE_OPTIONS=--max-old-space-size=4096 nuxt buildOr exported before the build command:
export NODE_OPTIONS=--max-old-space-size=4096
nuxt buildIn package.json:
{
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=4096 nuxt build"
}
}If you are on Windows or need the build to run on both Windows and Unix, install cross-env:
npm install --save-dev cross-env {
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 nuxt build"
}
}
Why not use the inline node invocation?
Older tutorials recommend calling Node.js directly: node --max-old-space-size=4096 node_modules/nuxt/bin/nuxt.js build. This still works if the binary path is correct. But NODE_OPTIONS is the portable method: it works regardless of package manager (npm, pnpm, Yarn), it does not break when Nuxt updates its binary location and it is the approach documented in official Node.js references as the standard way to pass runtime flags to spawned processes.
Choosing the right value:
- 512MB to 1GB available RAM: set
--max-old-space-size=512and add swap (covered below) - 1GB to 2GB available RAM:
--max-old-space-size=1536 - 2GB to 4GB available RAM:
--max-old-space-size=3072 - 4GB or more:
--max-old-space-size=6144
Do not set this value above roughly 80% of available physical RAM. The limit is a ceiling, not an allocation. Setting it above available RAM just delays the OOM kill slightly. The process will still be terminated when it actually tries to use that memory and the OS has none to give.
Fix 2: add swap space
If the server does not have enough physical RAM to complete the build even with a reasonable heap ceiling, swap gives the OS a pressure-relief valve. Swap is disk space treated as overflow RAM. The build will be slower (sometimes significantly slower) but it completes instead of being killed.
Most cloud VPS instances ship with swap disabled. The commands to create it are standard Linux administration and have not changed in years:
# Create a 4GB swap file
sudo fallocate -l 4G /swapfile
# Lock down permissions
sudo chmod 600 /swapfile
# Format as swap
sudo mkswap /swapfile
# Activate
sudo swapon /swapfile
# Verify
free -hTo persist across reboots, add this line to /etc/fstab:
/swapfile none swap sw 0 04GB is a reasonable starting size for most Nuxt 3 builds. On an SSD-backed VPS it adds latency but stays usable. On spinning disk it will be very slow. On NVMe it is nearly tolerable.
One practical constraint: swap solves the crash problem but it does not solve the time problem. A build that takes 90 seconds on a 4GB RAM machine may take 20 minutes with heavy swap usage. If you are deploying through a CI platform with a build time limit, you may hit that limit before the build finishes. Fix 1 and Fix 2 work best together: bump the heap limit first and add swap for headroom.
The CI and container angle
Netlify, Railway, Render and similar platforms execute your build command in their own containers. Setting NODE_OPTIONS in package.json works for these platforms because the build command they run is what triggers the env var.
For platform-level environment variable configuration (useful when you prefer not to modify package.json):
# Netlify: netlify.toml or the environment variable section in the dashboard
NODE_OPTIONS = --max-old-space-size=4096
# Railway / Render: environment variable settings in the service dashboard
NODE_OPTIONS=--max-old-space-size=4096For Docker-based builds, set the variable in the build stage:
FROM node:22-alpine AS builder
ENV NODE_OPTIONS=--max-old-space-size=4096
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run buildSet NODE_OPTIONS only in the builder stage. If you are using multi-stage Docker builds, the final runtime image does not need this flag and carrying it into production creates unnecessary confusion.
When it still crashes: specific culprits
If you have adjusted the heap limit, added swap and the build still dies, one of these is usually the cause.
Nuxt DevTools included in production builds. If Nuxt DevTools is installed and not explicitly disabled, it contributes build-time overhead. In nuxt.config.ts:
export default defineNuxtConfig({
devtools: { enabled: false }
})nuxt generate versus nuxt build. Static site generation pre-renders every route at build time. On a project with hundreds of dynamic routes, this can consume several times more memory than a standard nuxt build. If you are running nuxt generate on a low-memory server, consider whether server-side rendering with nuxt build is viable for your use case.
TypeScript strict mode on large codebases. Full TypeScript type checking is the single largest contributor to memory usage on most Nuxt 3 projects. You can disable it during builds while keeping type checking in your editor and CI type-check step:
// nuxt.config.ts
export default defineNuxtConfig({
typescript: {
typeCheck: false
}
})This is a genuine trade-off. Build-time type safety validation goes away in exchange for a substantially lower memory footprint. Most teams land on running type checking as a separate CI step and disabling it from the build command on resource-constrained environments.
Oversized icon or component libraries. Importing an entire icon set rather than individual icons forces the bundler to analyse a large module graph it will mostly tree-shake out. The analysis still costs memory. Switch to named imports:
// Before: imports everything for analysis, then tree-shakes
import * as Icons from 'some-icon-library'
// After: only analyses what is requested
import { ChevronRight, XMark } from 'some-icon-library'What about pnpm and Bun?
Two questions come up often enough to address directly.
pnpm as your package manager: The NODE_OPTIONS approach works identically with pnpm. Set the environment variable and run pnpm run build exactly as you would with npm. One caveat worth knowing: nuxt/nuxt discussion #28350 documents that switching from npm to pnpm in CI environments (specifically Azure DevOps) can increase build memory pressure and cause timeouts in certain workspace configurations. The culprit is usually pnpm's hoisting behavior conflicting with how Nuxt resolves modules. If you switch package managers and builds get worse, not better, that discussion is worth reading.
Bun: The distinction here matters. Bun as a package manager (running bun install to manage dependencies, then npm run build or pnpm run build for the actual build) has no meaningful effect on build memory. The Nuxt build process still runs on Node.js and V8 regardless of how you installed packages.
Bun as a runtime for the build itself (bun run build where Bun executes the build process) is a different situation entirely and not one to pursue on a low-memory server right now. As of 2026, there are active GitHub issues in the oven-sh/bun repository documenting memory growth specific to Nuxt + Bun that takes the process from a few hundred megabytes to several gigabytes within minutes. The Bun team is aware of these, but they are unresolved. If you are trying to fix a build that crashes due to memory limits, swapping in Bun as the runtime is more likely to make things worse than better.
When to stop patching
The heap flag and swap file buy time. They are not permanent solutions for a project that keeps growing.
If your project has more than 50 routes, uses server-side rendering, has a substantial component library and you are adjusting memory settings every few weeks as the codebase grows, the project has outgrown the server. A $14-a-month VPS with 2GB RAM and a 2GB swap file will handle builds that a $6 instance cannot.
The right frame is this: --max-old-space-size and swap should fix a crash that is happening today. If you are revisiting these settings on a regular basis, that is information about the project, not a configuration problem to solve. A project whose build requirements are growing is a project that needs a bigger build environment, not a longer sequence of workarounds.
Closing
Exit code 137 has been annoying Node.js developers for over a decade. The gap between “what the framework needs at build time” and “what budget infrastructure provides” has not closed. It has widened as Nuxt 3, Next.js and similar frameworks made their build pipelines more thorough and TypeScript adoption pushed type-checking overhead into nearly every project. The problem is not going away.
The fix hierarchy in 2026: set NODE_OPTIONS using the environment variable approach (not the old inline node invocation), add swap if the server needs overflow room, disable expensive optional build steps such as DevTools and runtime type checking if you need headroom and look at specific dependency packages if nothing else changes the outcome. If those adjustments are not holding, upgrade the build machine. The tooling has grown considerably more capable and considerably more memory-hungry over the past several years. The budget-versus-requirements trade-off has not gotten easier.


