Back to Article List

Fix Node.js heap and memory errors on a VPS

Fix Node.js heap and memory errors on a VPS - Fix Node.js heap and memory errors on a VPS

Understanding the error and what it means

You deploy your Node.js app to your VPS. A week later, your logs fill with this:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Your process crashes. The VPS keeps running, but your app stops serving requests. Now customers are complaining and you're scrambling to figure out whether the VPS is undersized or if your code has a leak.

This error means V8, Node's JavaScript engine, tried to allocate memory for new objects but couldn't. The garbage collector (GC) cleaned up what it could, but the remaining live objects filled the entire heap. V8 gave up and crashed the process.

The critical question: is this a capacity problem (too many legitimate objects for your VPS) or a leak problem (objects that should be garbage-collected but aren't). The answer determines your fix.

How V8 heap allocation and garbage collection work

Node.js uses the V8 JavaScript engine, which manages memory through a garbage collector. The heap is divided into two main regions: the young generation (collected frequently) and the old generation (collected less often). Objects start in young generation and move to old generation if they survive multiple garbage collections.

When your code allocates objects, variables, arrays and data structures, they live in the heap until the GC determines they're unreachable. Unreachable objects are reclaimed and the memory is reused.

Young generation vs old generation heaps

The young generation is small (around 16-32 MB) and collected very frequently. Most objects die here within milliseconds. The old generation is much larger (up to several GB) and collected less often. Objects that survive 2-3 GC cycles in young generation get promoted to old generation.

This two-level approach is efficient. Short-lived objects (like temporary variables in request handlers) are collected quickly without the overhead of managing long-lived objects.

Default heap limits by VPS size

V8 sets a maximum heap size based on available system memory at startup, but it's capped as a safety feature to prevent the OS from freezing when Node swallows all RAM.

On a 512 MB VPS, V8 typically allocates about 256 MB max heap. On a 2 GB VPS, maybe 1 GB. On a 4 GB VPS, around 2-2.5 GB. These are conservative estimates; the exact values depend on Node.js version and your configuration.

The garbage collector makes decisions based on heap occupancy. When the heap is 70% full, the GC runs. When it's 90% full, the GC runs more aggressively. At 100%, the engine throws the out-of-memory error.

Check your current heap limits

First, see what your Node process actually has available. Run this command:

node -e "console.log(require('v8').getHeapStatistics())"

Look at the output for heap_size_limit. That's your ceiling. If it's under 500 MB and your app is crashing, that's your first clue.

You can also check heap usage at runtime with:

const mem = process.memoryUsage();
console.log('Heap used:', Math.round(mem.heapUsed / 1024 / 1024), 'MB');
console.log('Heap limit:', Math.round(mem.heapTotal / 1024 / 1024), 'MB');

Run this periodically (or expose it on a /debug endpoint in dev) to watch memory growth over time. If heap usage climbs steadily and approaches the limit, you have a leak.

Increasing the heap with the --max-old-space-size flag

The easiest short-term fix is to tell V8 to use more memory. The flag is --max-old-space-size, measured in megabytes.

node --max-old-space-size=2048 app.js

This sets the old generation heap limit to 2 GB. But here's the critical caveat: you can't give Node more memory than your VPS actually has. If you set it to 3 GB on a 1 GB VPS, the OS will kill the process when physical RAM runs out and the kernel can't allocate more memory pages.

A safe rule of thumb: set it to about 75% of your VPS RAM. On a 2 GB VPS, use 1500 MB. On a 4 GB VPS, use 3000 MB. Leave the rest for the OS, Nginx, PostgreSQL and other processes.

Applying the heap flag with PM2

You're probably running your app with PM2, which comes pre-installed on LumaDock Node.js VPS instances. Pass the heap flag via your ecosystem configuration file:

module.exports = {
  apps: [
    {
      name: 'my-app',
      script: './app.js',
      instances: 'max',
      exec_mode: 'cluster',
      node_args: '--max-old-space-size=2048'
    }
  ]
};

Then start with pm2 start ecosystem.config.js. If node_args doesn't work (older PM2 versions), try the args field instead.

After restarting, verify the flag took effect:

ps aux | grep node | grep max-old-space-size

You should see the flag in the command line.

Using the NODE_OPTIONS environment variable

Alternatively, set the NODE_OPTIONS environment variable:

export NODE_OPTIONS="--max-old-space-size=2048"
node app.js

This works in Docker containers and systemd services. In a PM2 ecosystem file, you can also use it:

module.exports = {
  apps: [
    {
      name: 'my-app',
      script: './app.js',
      env: {
        NODE_OPTIONS: '--max-old-space-size=2048'
      }
    }
  ]
};

The node_args approach is cleaner, but both work. Environment variables can be overridden at deploy time if needed.

Diagnosing memory leaks vs normal growth

Increasing the heap buys you time, but it's not a fix if you have a leak. Memory will keep climbing until it hits the new limit. You need to find what's leaking.

Start with a baseline. Restart your app cleanly and measure memory usage when idle:

node -e "console.log(JSON.stringify(process.memoryUsage()))" > baseline.txt

# Run your app
node app.js &
APP_PID=$!

# Wait and measure
sleep 60
kill $APP_PID
node -e "console.log(JSON.stringify(process.memoryUsage()))" > after60s.txt

Compare the two files. If heap usage grew by more than 10-20% in 60 seconds under light load, that's suspicious. It suggests objects are accumulating without being garbage-collected.

Baseline measurement technique

A better approach is to measure heap usage at consistent intervals under realistic traffic. Set up a logging endpoint that exports current memory stats:

app.get('/memory-stats', (req, res) => {
  const mem = process.memoryUsage();
  res.json({
    heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
    heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
    rss: Math.round(mem.rss / 1024 / 1024),
    timestamp: new Date().toISOString()
  });
});

Query this endpoint every hour and log the results. If heap usage climbs consistently, you have a leak.

Heap snapshots with --inspect and Chrome DevTools

The nuclear option for leak detection is a heap snapshot. Start your app with the inspect flag:

node --inspect app.js

The output will say something like Debugger listening on ws://127.0.0.1:9229/.... Open Chrome and navigate to chrome://inspect. You'll see your Node process listed. Click "inspect".

In Chrome DevTools, go to the Memory tab and click "Take heap snapshot". The snapshot captures every object in memory and what's holding references to it. This is thorough but requires detective work.

Finding leak patterns with heap snapshot comparison

A single snapshot shows objects in memory, but doesn't reveal what's leaking. Take two snapshots with time between them:

1. Start your app, take a baseline snapshot (snapshot A)
2. Generate some load for 5-10 minutes
3. Take a second snapshot (snapshot B)
4. In Chrome DevTools, click the dropdown (usually showing "Summary") and select "Comparison"
5. Select snapshot A to compare against

The comparison view shows objects created between the two snapshots that are still alive. Look for "Size Delta" column to spot growing object counts. Click on an object type to drill down into what's holding references.

Common leak patterns include:

Event listeners: Listeners attached to objects but never removed, preventing garbage collection.
Unbounded arrays: Arrays that keep growing (caches, logs, request queues) without cleanup.
Closures: Functions that capture large variables in their scope and never go out of scope.
Global caches: Objects stored on globals or modules that are never pruned.

Using Clinic.js for automated memory profiling

Manual heap snapshot analysis is tedious. Clinic.js automates the process:

npm install -g clinic

Run your app through Clinic's doctor:

clinic doctor -- node app.js

Generate some load, then stop the app (Ctrl+C). Clinic generates an HTML report showing CPU usage, memory usage and event loop performance. If memory climbs steadily, Clinic flags it as a potential leak.

Clinic also has a memory profiler:

clinic memory -- node app.js

This captures heap snapshots automatically and compares them, showing you the allocation timeline and what's growing. The output is an interactive HTML dashboard.

PM2 max_memory_restart as a temporary fix

PM2 has a max_memory_restart option that restarts your app when memory exceeds a threshold:

node_args: '--max-old-space-size=2048',
max_memory_restart: '1536M'

This is useful for hiding a memory leak in production (the app restarts automatically before it crashes), but it's not a fix. You're still losing state on restart. Only use this as a temporary band-aid while you hunt down the real leak.

When to upgrade your VPS vs when to fix your code

Here's my decision framework: if your app uses less than 1 GB of heap on a well-tuned instance and memory is stable over 24 hours, your current VPS is fine. If you're consistently using 80% of your heap and memory keeps climbing, you need to decide.

Option one: your app genuinely needs more memory. Maybe you're caching a large dataset, processing huge files, or running intensive computations. In that case, upgrading your VPS is the right call. Measure your actual peak usage, add 20% headroom, and size accordingly.

Option two: you have a leak or inefficiency in your code. Use heap snapshots, clinic.js or Node's built-in profiler to find it. Common fixes include cleaning up event listeners, limiting cache sizes, closing unused database connections, or refactoring closures to avoid capturing large variables.

The honest answer: do both. Increase the heap to keep production stable while you investigate. Most apps leak in subtle ways you won't catch in testing. A few hours with heap snapshots often uncovers issues that cost money if you only upgrade the VPS.

Streams and chunked processing for large data

If your app processes large files or datasets, avoid loading everything into memory at once. Use streams instead:

const fs = require('fs');

// Bad: loads entire file into memory
const data = fs.readFileSync('huge-file.csv', 'utf8');
const lines = data.split('\n');

// Good: streams the file, processing chunks
fs.createReadStream('huge-file.csv')
  .on('data', (chunk) => {
    const lines = chunk.toString().split('\n');
    lines.forEach(line => processLine(line));
  })
  .on('end', () => console.log('Done'));

Streams process data in 64 KB chunks by default, keeping memory usage constant regardless of file size. This is critical for production apps handling user uploads or large API responses.

Monitoring memory trends with Prometheus and Grafana

For long-term monitoring and trending, set up Prometheus and Grafana. They capture memory metrics over days and weeks, making leaks obvious.

With Prometheus scraping metrics every 15 seconds, you can spot trends. If heap usage climbs by 10 MB per hour, that's a leak. If it plateaus after 100 MB and stays stable, that's normal. Grafana dashboards make this visual and obvious.

See the Node.js monitoring guide for setup instructions.

And for scaling beyond a single instance, learn about PM2 clustering to balance load across multiple Node processes.

Your idea deserves better hosting

24/7 support 30-day money-back guarantee Cancel anytime
Billing Cycle

1 GB RAM VPS

14.49 zł Save  25 %
10.86 Monthly
  • 1 vCPU AMD EPYC
  • 30 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Firewall management
  • Free server monitoring

2 GB RAM VPS

21.75 zł Save  17 %
18.12 Monthly
  • 2 vCPU AMD EPYC
  • 30 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Firewall management
  • Free server monitoring

6 GB RAM VPS

54.42 zł Save  33 %
36.27 Monthly
  • 6 vCPU AMD EPYC
  • 70 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P1

29.01 zł Save  25 %
21.75 Monthly
  • 2 vCPU AMD EPYC
  • 4 GB RAM memory
  • 40 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P2

54.42 zł Save  27 %
39.90 Monthly
  • 2 vCPU AMD EPYC
  • 8 GB RAM memory
  • 80 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P4

108.88 zł Save  20 %
87.10 Monthly
  • 4 vCPU AMD EPYC
  • 16 GB RAM memory
  • 160 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P5

132.48 zł Save  21 %
105.25 Monthly
  • 8 vCPU AMD EPYC
  • 16 GB RAM memory
  • 180 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P6

206.91 zł Save  21 %
163.34 Monthly
  • 8 vCPU AMD EPYC
  • 32 GB RAM memory
  • 200 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

AMD EPYC VPS.P7

254.11 zł Save  20 %
203.28 Monthly
  • 16 vCPU AMD EPYC
  • 32 GB RAM memory
  • 240 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

EPYC Genoa VPS.G1

18.12 zł Save  20 %
14.49 Monthly
  • 1 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture.
  • 1 GB DDR5 memory
  • 25 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

EPYC Genoa VPS.G2

47.16 zł Save  23 %
36.27 Monthly
  • 2 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture.
  • 4 GB DDR5 memory
  • 50 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

EPYC Genoa VPS.G4

94.36 zł Save  27 %
68.95 Monthly
  • 4 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture.
  • 8 GB DDR5 memory
  • 100 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

EPYC Genoa VPS.G6

177.86 zł Save  31 %
123.41 Monthly
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture.
  • 16 GB DDR5 memory
  • 200 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

EPYC Genoa VPS.G7

272.26 zł Save  27 %
199.65 Monthly
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture.
  • 32 GB DDR5 memory
  • 250 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

1 vCPU AMD Ryzen 9

50.79 zł Save  29 %
36.27 Monthly
  • Dedicated CPU 4.5GHz AMD Ryzen 9 7950X with a native CPU frequency of 4.5 GHz.
  • 4 GB DDR5 memory
  • 50 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

2 vCPU AMD Ryzen 9

94.36 zł Save  19 %
76.21 Monthly
  • Dedicated CPU 4.5 GHz AMD Ryzen 9 7950X with a native frequency of 4.5 GHz.
  • 8 GB DDR5 memory
  • 100 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

8 vCPU AMD Ryzen 9

337.61 zł Save  30 %
235.96 Monthly
  • Dedicated CPU 4.5 GHz AMD Ryzen 9 7950X with a native frequency of 4.5 GHz.
  • 32 GB DDR5 memory
  • 400 GB NVMe storage
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • 1 Gbps network
  • Automatic backup included
  • Firewall management
  • Free server monitoring

FAQ

How do I check my app's memory usage from within the code?

Use process.memoryUsage(), which returns an object with heapUsed, heapTotal, rss (resident set size) and external bytes. Log this to your monitoring system. In an Express app, expose a /metrics endpoint that returns these values, then query it regularly.

Skip the setup, start deploying

Your server comes ready with PM2, Nginx and Certbot. Pick a datacenter and push your first build in minutes.

GPU products are in high demand at the moment. Fill the form to get notified as soon as your preferred GPU server is back in stock.