Fix Node.js GC memory spikes that freeze your app

Programming & Dev Tools Intermediate 👁 5 views 📅 Jun 30, 2026

Node.js garbage collection can eat up CPU and memory, freezing your app. Here's how to tame it fast.

This error is annoying, I know

You're running a Node.js app, maybe Express or a background worker, and suddenly CPU hits 100%. Your app freezes for seconds. You check the logs — it's garbage collection (GC) taking over. I've been there. It's especially bad with large datasets or long-running processes like cron jobs or WebSocket servers.

The quick fix: tune V8's GC flags

Node.js uses V8's garbage collector. By default, it's conservative — it waits until memory is nearly full, then does a big pause. You can change that. Here's the fix:

node --max-old-space-size=512 --optimize-for-size --gc-interval=100 your-app.js

What these flags do:

  • --max-old-space-size: Sets the max heap size in MB. Lower it to force more frequent, smaller GC cycles. Start with 256 or 512. If your app uses 1GB normally, set it to 1024.
  • --optimize-for-size: Tells V8 to prefer memory over speed. Counter-intuitive, but it reduces pause times.
  • --gc-interval: Forces a GC cycle every N milliseconds. I use 100 for responsive apps. This stops big pauses entirely.

Try this in dev first. On a production server, I run it like this:

NODE_OPTIONS="--max-old-space-size=512 --optimize-for-size --gc-interval=100" node app.js

Why this works

V8's default GC is generational — it collects young objects quickly, but old objects (like cached data or large arrays) build up. When old space hits its limit, V8 does a full mark-and-sweep. That's the pause. By shrinking the old space limit and forcing small GC runs, you trade memory for smoother CPU usage. No more 5-second freezes.

Less common variations of this problem

Not all memory spikes come from default GC settings. Here are a few I've seen:

Memory leak in event listeners

If your app adds event listeners (like process.on('data') or WebSocket handlers) without removing them, memory grows slowly. GC then tries to clean it but can't, causing bigger and bigger pauses. Fix: use removeListener or libraries like EventEmitter2 that auto-clean.

Large JSON payloads

Parsing a 10MB JSON response in a loop? That creates a ton of temporary objects. V8's GC will spike. Fix: stream the JSON with JSONStream or stream-json instead of JSON.parse.

Using global variables

I once found a colleague storing session data in a global Map. It never got freed. GC kept marking it. Fix: use WeakMap or clear manually.

Third-party modules with bad memory patterns

Some packages (like old versions of mongoose or request) allocate large buffers that resist GC. Check your dependencies with node --trace-gc to see patterns.

How to prevent this from happening again

Prevention is better than tuning. Here's what I do:

  1. Monitor GC stats: Run node --trace-gc app.js and log output. Look for pauses longer than 100ms. Tools like Clinic.js or heapdump can visualize this.
  2. Use --max-old-space-size early: Set it before you hit production. Test with realistic data sizes.
  3. Profile memory leaks: Run node --inspect and use Chrome DevTools Memory tab. Take heap snapshots before and after operations. If an object grows, it's a leak.
  4. Avoid creating objects in hot paths: Cache results, reuse arrays, use Buffer.alloc instead of new Buffer. Every allocation adds GC pressure.
  5. Use the --expose-gc flag: Call global.gc() manually in some cases (like after a batch job). Be careful — it blocks the event loop.

One last thing: if you're running containers, set memory limits with Docker or cgroups. GC behaves differently when memory is capped. For a 512MB container, start with --max-old-space-size=384.

Try this today. Your app will thank you.

Was this solution helpful?