Org Skills

nodejs-performance

Diagnose and fix Node.js back-end performance problems — event loop blocking and lag, high latency and low throughput, CPU hot paths, memory leaks and OOM kills, stream backpressure, JSON serialization cost, slow queries and pool exhaustion, caching, HTTP keep-alive and scaling in containers.

Download .zip Raw Source
When agents use itUse this whenever the user says a Node/TypeScript API is slow, p99 latency spikes, CPU sits at 100%, memory keeps growing, the pod gets OOMKilled, "JavaScript heap out of memory", event loop lag or utilization alerts fire, requests time out under load, or asks about clinic.js, 0x, --cpu-prof, --inspect, heap snapshots, autocannon, k6, perf_hooks, worker_threads, cluster, PM2, UV_THREADPOOL_SIZE, --max-old-space-size or undici agents. Also apply it when reviewing Node.js code for hot-path issues such as sync I/O, unbounded caches, large JSON.parse/stringify, N+1 queries or missing timeouts in server.ts or handlers.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill nodejs-performance -a github-copilot
# or with the org installer (adds .github/skills/nodejs-performance):
npx -y github:AGCO-Global/org-skills add skill nodejs-performance

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill nodejs-performance
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill nodejs-performance -g

Installs the backend-nodejs-skills plugin, which bundles all Backend / Node.js skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-nodejs-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill nodejs-performance -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill nodejs-performance -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-performance -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-performance -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Node.js Performance

Node is fast until something occupies the single JavaScript thread; then every request waits. Measure under realistic load, find the one bottleneck (CPU on the loop, waiting on I/O, memory/GC, or downstream), fix that, and re-measure. Never tune GC flags or add clustering before you have a profile.

1. Classify the problem with numbers

Signal Collect Meaning
Event loop delay perf_hooks.monitorEventLoopDelay() p99 > ~50–100 ms means something blocks the loop
Event loop utilization performance.eventLoopUtilization() Near 1.0 = CPU-bound on the main thread; low ELU + high latency = waiting on I/O/downstream
CPU per process container metrics ~100% of one core with low ELU headroom = loop saturated
Heap used vs limit process.memoryUsage(), v8.getHeapStatistics() Sawtooth = normal GC; rising floor = leak
Latency by span OpenTelemetry traces Which dependency or step dominates
Pool wait ORM/driver pool metrics Requests queueing for DB connections
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
const h = monitorEventLoopDelay({ resolution: 20 }); h.enable();
let last = performance.eventLoopUtilization();
setInterval(() => {
  const elu = performance.eventLoopUtilization(last); last = performance.eventLoopUtilization();
  log.info({ elu: elu.utilization, lagP99ms: h.percentile(99) / 1e6 }); h.reset();
}, 10_000).unref();

2. Reproduce and profile

  • Load test with autocannon (quick, single endpoint) or k6 (scenarios, ramping, thresholds) against a production build with production-like data volume. Record p50/p95/p99, throughput and error rate as the baseline.
  • CPU: node --cpu-prof dist/server.js (open .cpuprofile in DevTools), 0x flame graphs, or node --inspect + Chrome DevTools Performance. clinic.js (doctor/flame/bubbleprof) gives a guided first look; check it supports your Node version.
  • Memory: --heapsnapshot-signal=SIGUSR2 or v8.writeHeapSnapshot(); --heap-prof for allocation sampling.
  • Run the profiler under load, not idle; source maps on (--enable-source-maps) so frames map to TS.

3. Triage table

Symptom Likely cause Check Fix
All endpoints slow together, high ELU Loop blocked by sync work CPU profile: JSON.parse, regex, *Sync, crypto, big loops Stream/chunk, move to worker_threads, async APIs
p99 spikes, periodic GC pauses or large allocations --trace-gc, heap sampling Reduce allocation, avoid giant arrays/strings; paginate
Latency high, ELU low Waiting on DB/HTTP/pool Traces, pool wait metrics Fix queries/indexes, size pool, parallelize independent calls
Slow bcrypt/fs/zlib/dns.lookup under load libuv threadpool (default 4) saturated Concurrency vs UV_THREADPOOL_SIZE Raise UV_THREADPOOL_SIZE, cache DNS, reduce concurrent heavy ops
Memory floor rises until OOM Leak: unbounded cache, listeners, closures, global maps Compare 3 heap snapshots over time Bound caches, remove listeners, fix retention
RSS high, heap normal Buffers, native memory, streams not drained memoryUsage().arrayBuffers/external Backpressure with pipeline, stream instead of buffering
Outbound calls slow, many sockets No keep-alive / new client per request Socket counts, TLS handshakes in traces Shared agent/dispatcher with keep-alive
502s behind load balancer Server keepAliveTimeout shorter than LB idle timeout Compare timeouts Set server.keepAliveTimeout above LB idle timeout, headersTimeout above that
Throughput flat as pods scale Downstream (DB) is the bottleneck DB CPU, connections, locks Query/index work, caching, read replicas

4. CPU-heavy work

  • Anything over a few milliseconds of synchronous CPU per request (image processing, PDF, large CSV/JSON transforms, hashing loops) goes to worker_threads via a pool (piscina) or to a separate queue-fed worker service.
  • Break unavoidable in-loop work into chunks with setImmediate yields only as a stopgap.
  • Beware catastrophic regex backtracking on user input; bound input sizes and use linear-time patterns.
import Piscina from 'piscina';
const pool = new Piscina({ filename: new URL('./resize.worker.js', import.meta.url).href });
app.post('/thumbnails', async (req) => pool.run({ key: req.body.key }));

5. Memory leaks

Common causes: module-level Map/object caches without eviction, event listeners added per request (MaxListenersExceededWarning is a clue), closures capturing request objects in long-lived callbacks, timers never cleared, unbounded in-memory queues, AsyncLocalStorage stores holding large objects.

Method: take a snapshot after warm-up, run load, take two more; compare "Objects allocated between snapshots" and follow retainers to the root. Set --max-old-space-size explicitly to roughly 70–80% of the container memory limit so V8 collects before the kernel kills the process.

6. I/O, streams and serialization

  • Stream large payloads with stream/promises pipeline(); it propagates errors and respects backpressure. Never await res.text() a multi-MB body just to forward it.
  • Honour write() returning false (wait for 'drain') when writing manually.
  • JSON is often the top CPU cost: Fastify response schemas compile serializers (fast-json-stringify) and drop unlisted fields; return only needed fields; paginate lists; avoid JSON.parse(JSON.stringify(x)) cloning (use structuredClone or don't clone).
  • Compression (gzip/brotli) is CPU work: offload to the reverse proxy/CDN where possible.

7. Database, caching, outbound HTTP

  • Kill N+1 (ORM includes/joins or batching with DataLoader), add indexes from EXPLAIN ANALYZE, select only needed columns, paginate with keysets.
  • Pool size is per process: replicas × processes × pool must stay below DB connection limits; pooling proxy for serverless.
  • Parallelize independent awaits with Promise.all; cap fan-out concurrency (p-limit).
  • Caching: in-process LRU (lru-cache with max and ttl) for hot, small, per-instance data; Redis for shared or larger data and cross-instance invalidation. Add stampede protection (request coalescing, jittered TTLs). Every cache has a bound and an invalidation story.
  • Outbound: reuse one keep-alive agent/undici dispatcher per upstream (new Agent({ keepAliveTimeout, connections })), set timeouts, and don't create clients per request.

8. Scaling and runtime flags

  • In containers, prefer one Node process per container and scale horizontally; give it ~1 CPU. Use cluster/PM2 only on VMs or when a pod must use several cores and you accept the extra memory and complexity.
  • Keep Node on the current LTS; upgrades often bring V8 and undici gains for free.
  • GC flags (--max-semi-space-size for allocation-heavy services, --max-old-space-size) only with before/after measurements; change one at a time.

Deliverable format

## Baseline      load profile, p50/p95/p99, RPS, error rate, ELU, heap, tool used
## Diagnosis     triage row matched, evidence (profile frames, snapshot retainers, traces)
## Fixes         ordered by impact; minimal code/config diff for each
## Result        same load test re-run; before/after table
## Guardrails    alerts/metrics added (loop lag, ELU, heap, pool wait) and load test in CI

Anti-patterns to reject

  • Tuning or adding clustering/replicas without a profile.
  • *Sync fs/crypto/zlib calls, big JSON.parse or regex on user input in request handlers.
  • Unbounded Map caches, per-request listeners, timers without clearTimeout/unref.
  • Buffering whole files or responses in memory instead of streaming.
  • New HTTP client, DB pool or Redis connection per request.
  • cluster inside Kubernetes pods by default; --max-old-space-size larger than the container limit.
  • Measuring with the dev server, tsx watch mode or an empty database.