OOM Killer Response
When Kubernetes marks your Node.js pod OOMKilled, you have minutes to decide: leak, traffic spike, or misconfigured limits. This page triages heap vs RSS and picks the right mitigation.
Search across all documentation pages
When Kubernetes marks your Node.js pod OOMKilled, you have minutes to decide: leak, traffic spike, or misconfigured limits. This page triages heap vs RSS and picks the right mitigation.
Quick-reference recipe card - copy-paste ready.
# 1. Confirm OOMKilled (not CrashLoop for other reasons)
kubectl describe pod <pod> -n production | grep -E "OOMKilled|Reason|Exit Code"
# 2. Check memory metrics: heap vs RSS
# Grafana: process_resident_memory_bytes vs v8 heap used
# 3. Correlate with deploy and traffic
kubectl rollout history deployment/<svc>
# Compare RPS spike to memory slope
# 4. Mitigate
kubectl scale deployment/<svc> --replicas=<N+2> # traffic spike
kubectl rollout undo deployment/<svc> # bad deploy
# leak: throttle traffic, rolling restart, schedule heap snapshotWhen to reach for this:
OOMKilled and Exit Code 137heapUsed stays flat (native buffers, gRPC, sharp)// src/observability/memory-metrics.ts
import { monitorEventLoopDelay } from "node:perf_hooks";
import v8 from "node:v8";
const loopDelay = monitorEventLoopDelay({ resolution: 20 });
loopDelay.enable();
export function startMemoryReporter(log: (obj: object) => void, intervalMs = 30_000) {
setInterval(() => {
const mem = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
log({
msg: "memory_sample",
rssMb: Math.round(mem.rss / 1024 / 1024),
heapUsedMb: Math.round(mem.heapUsed / 1024 / 1024),
heapTotalMb: Math.round(mem.heapTotal / 1024 / 1024),
externalMb: Math.round(mem.external / 1024 / 1024),
arrayBuffersMb: Math.round(mem.arrayBuffers / 1024 / 1024),
heapLimitMb: Math.round(heapStats.heap_size_limit / 1024 / 1024),
eventLoopP99Ms: Math.round(loopDelay.percentile(99) / 1e6),
});
loopDelay.reset();
}, intervalMs).unref();
}What this demonstrates:
rss vs heapUsed divergence signals native/buffer growthexternal and arrayBuffers track Buffer and TypedArray pressure| Metric | What it measures | Grows when |
|---|---|---|
heapUsed | V8 JavaScript objects | Leaked closures, growing Maps, cached objects |
rss | Total process resident memory | Heap + native addons + thread stacks + mmap |
external | C++ objects bound to JS | Buffers, native crypto, some ORM drivers |
arrayBuffers | Detached ArrayBuffer memory | Binary payloads, file uploads |
| Signal | Likely leak | Likely traffic/capacity |
|---|---|---|
| Traffic | Flat RPS | RPS up 2-10x |
| Memory slope | Linear over hours | Step change at spike |
| Restarts | Same pod, same pattern | All pods, proportional load |
heapUsed | Tracks RSS up | May stay moderate |
| Fix | Code fix + snapshot | Scale HPA, tune pool, raise limits |
OOMKilled?
├─ Deploy in last 60 min? → rollback first
├─ RPS spike? → scale replicas, check pool sizes
├─ heapUsed ≈ RSS climbing? → leak hunt (snapshot, clinic heapprofiler)
├─ RSS >> heapUsed? → check Buffers, sharp, gRPC, Prisma engine
└─ Limits too tight? → raise memory request/limit with evidenceresources:
requests:
memory: "512Mi"
limits:
memory: "768Mi" # headroom above steady-state RSSNODE_OPTIONS=--max-old-space-size=512 below k8s limit (leave ~30% for non-heap)heapdump stalls the event loop. Fix: Copy pod, drain traffic, snapshot on the clone.external - Leak detectors watch heap only. Fix: Log arrayBuffers and external every 30s.Map keyed by userId grows forever. Fix: LRU with max entries or Redis.| Alternative | Use When | Don't Use When |
|---|---|---|
clinic heapprofiler | Reproducible leak in staging | Production under customer load |
clinic doctor | Event-loop + memory combined | You already know it is OOM |
| Raise limits only | Proven capacity spike | RSS climbs under flat traffic |
| Switch to worker threads | CPU-bound blocking loop | I/O-bound API (won't fix leak) |
137 (128 + 9 SIGKILL). Kubernetes events show OOMKilled as the reason.
Use kill -USR2 <pid> if wired to heapdump, or exec into a drained pod. Never snapshot the only replica under full traffic.
They cause exhaustion and timeouts before OOM, but idle pool growth increases RSS. Check pg pool totalCount vs max.
Only in controlled diagnostics. Forcing GC hides leak signals in metrics.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026