Fix Node.js GC memory spikes that freeze your app
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.jsWhat 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.jsWhy 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:
- Monitor GC stats: Run
node --trace-gc app.jsand log output. Look for pauses longer than 100ms. Tools like Clinic.js orheapdumpcan visualize this. - Use --max-old-space-size early: Set it before you hit production. Test with realistic data sizes.
- Profile memory leaks: Run
node --inspectand use Chrome DevTools Memory tab. Take heap snapshots before and after operations. If an object grows, it's a leak. - Avoid creating objects in hot paths: Cache results, reuse arrays, use
Buffer.allocinstead ofnew Buffer. Every allocation adds GC pressure. - Use the
--expose-gcflag: Callglobal.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?