Tuning the Garbage Collector
V8 exposes several command-line flags that adjust garbage collector behaviour. Most production Node applications run fine with defaults — but if you've profiled your app and found that GC pauses are causing latency spikes, or that heap growth is contributing to OOM crashes, the flags on this page give you levers to pull. The rule is: measure first, tune second. Flags that help one workload can hurt another. This page covers the flags that matter most, what each controls, and the workload characteristics that make tuning worthwhile.
Most applications do not need GC tuning
Key GC flags
Flag | Default | Effect |
|---|---|---|
| ~1400–1500 MB (64-bit) | Hard ceiling on Old Space heap size; process exits with OOM if exceeded |
| 8 MB | New Space semi-space size; larger = fewer minor GCs but more memory per promotion survivor |
| V8 chooses | Pre-allocate heap at startup; avoids growth overhead for known-large workloads |
| off | Prefer smaller memory footprint over throughput; trades some speed for lower memory |
| off (dev only) | Exposes |
| off | Logs each GC event with type, heap sizes before/after, and duration |
| off | More detailed GC logging including pause reasons |
Adjusting heap size limits
# Raise Old Space ceiling for memory-intensive batch jobs: node --max-old-space-size=4096 batch-processor.js # Set via environment variable (useful in Docker/Kubernetes): NODE_OPTIONS="--max-old-space-size=2048" node server.js # Increase New Space to reduce minor GC frequency for allocation-heavy workloads: node --max-semi-space-size=32 server.js # 32 MB semi-space (from default 8 MB) # Doubles New Space total (two semi-spaces = 64 MB instead of 16 MB) # Fewer minor GCs, but more memory; check if survivors also increase
Tuning New Space size
// Diagnose minor GC frequency first — count Scavenge events per second:
import { PerformanceObserver, constants } from 'node:perf_hooks'
let scavengeCount = 0
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if ((entry.detail as any).kind === constants.NODE_PERFORMANCE_GC_MINOR) scavengeCount++
}
})
obs.observe({ entryTypes: ['gc'] })
setInterval(() => {
console.log(`Minor GC rate: ${scavengeCount}/s`)
scavengeCount = 0
}, 1000)
// If you see 50+ minor GCs/sec under load:
// → Increase --max-semi-space-size (16, 32, 64 MB) and re-measure
// → Also check if you can reduce allocation rate (reuse objects, use object pools)Object pooling — reduce allocation pressure
// For hot paths that allocate many identical objects, reuse them:
class BufferPool {
private pool: Buffer[] = []
private readonly size: number
constructor(bufferSize: number, private readonly maxSize = 100) {
this.size = bufferSize
}
acquire(): Buffer {
return this.pool.pop() ?? Buffer.allocUnsafe(this.size)
}
release(buf: Buffer): void {
if (this.pool.length < this.maxSize) {
this.pool.push(buf)
}
// If pool is full, let the buffer be GC'd
}
}
const pool = new BufferPool(4096, 50) // pool of 4KB buffers
async function processRequest() {
const buf = pool.acquire()
try {
// ... use buf
} finally {
pool.release(buf)
}
}Exposing GC for tests
# Only use --expose-gc in tests to verify cleanup logic: node --expose-gc test-memory.js
// In a test file — verify that objects are collected when expected:
declare const gc: () => void // available only with --expose-gc
test('cache does not leak after clear', () => {
const cache = new WeakRef(buildExpensiveCache())
// ... use and clear the cache
gc() // force a GC cycle
expect(cache.deref()).toBeUndefined() // should have been collected
})GC tuning checklist
Run with
--trace-gcand verify GC is the actual cause of latency before changing any flags.Fix memory leaks first — a leaking heap will OOM at any limit.
Set
--max-old-space-sizeto ≤75% of available/container RAM; never exceed physical memory.If minor GC rate is very high (50+/sec), try doubling
--max-semi-space-sizeand re-measure.Use
--initial-heap-sizefor batch jobs that start with known large data to avoid incremental heap growth.Pool allocations on hot paths only after profiling confirms allocation rate is the bottleneck.
Never use
--expose-gcoutside test environments.