Performance Best Practices
Profile production-like concurrency, not hello-world curls. These rules keep Node.js APIs fast under real traffic.
How to Use This List
- Establish baseline p95/RPS with k6 before any optimization sprint.
- Pair load tests with clinic.js when latency regresses without obvious errors.
- Review before shipping list endpoints, file uploads, or new middleware chains.
- Re-run thresholds in CI after dependency upgrades.
A - Measure First
- Record p95 latency, error rate, and RPS before changing code. Numbers beat intuition in PR discussions.
- Use sustained concurrency (50+ VUs), not single curls. Blocks hide until requests overlap.
- Export event loop delay p99 from
monitorEventLoopDelay. Loop lag predicts user pain early. - Set k6 thresholds in CI for smoke scenarios. Fail merges when p95 regresses beyond SLO.
- Benchmark staging with production-shaped data volume. Empty DBs lie about query and serialize cost.
B - Main Thread Hygiene
- No CPU-heavy sync work on the request path. Move hashing, PDF generation, and image resize to workers.
- Cap JSON body size at the parser (
limit: "1mb"or stricter). Reject abuse beforeJSON.parseruns. - Slim response shapes at DB and mapper layers. Never serialize full ORM entities to clients.
- Avoid
*Syncfs and crypto APIs in HTTP handlers. Use promise/async variants. - Disable verbose logging and
pino-prettyduring profiling. Log I/O skews clinic and load results.
C - Serialization and Memory
- Use JSON Schema + fast-json-stringify on hot read paths. Fastify does this by default when schemas are set.
- Paginate list endpoints. Large arrays multiply serialize and GC cost.
- Fix heap leaks before raising
--max-old-space-size. Snapshots in staging, not guesswork. - Set heap to ~60-70% of container memory limit. Leave headroom for native buffers and TLS.
- Stream large downloads instead of buffering in memory. Chunked responses stabilize RSS.
D - Tooling and Profiling
- Run clinic Doctor first, Flame for CPU, Bubbleprof for async. Match tool to symptom.
- Generate load during Flame capture (autocannon or k6). Idle servers produce empty graphs.
- Profile compiled production builds when possible. Source maps improve symbol names.
- Document saturation point (VUs where p99 spikes). Autoscaling and pool sizing depend on it.
- Re-profile after Node major upgrades. GC and HTTP stack behavior changes between LTS lines.
E - Architecture and Scale
- Parallelize independent I/O with
Promise.all. Serialawaitchains add latency linearly. - Right-size DB and HTTP connection pools. Pool wait shows up as app latency, not DB CPU.
- Know when Express tuning is not enough. See Performance on Express vs Fastify ADR.
- Scale replicas for I/O-bound work; workers for CPU-bound work. Horizontal scale does not fix main-thread blocks.
- Define performance SLOs alongside availability. Tie to error budgets in SLOs & Error Budgets.
FAQs
What is the first metric to watch?
p95 request latency at expected concurrency, plus error rate. CPU alone misleads when you are I/O bound.
When is optimization premature?
When you have no baseline, no SLO, and no user-reported pain. Measure first.
Should every route have a JSON Schema?
Prioritize high-RPS read endpoints and large list responses. Low-traffic admin routes can wait.
How often to load test?
Smoke on every deploy to staging; full stress nightly or before major releases.
Does HTTP/2 fix latency?
Helps connection reuse; does not fix sync JSON or serial DB calls. Profile the handler.
What about caching?
Redis/CDN caching is valid - still slim origin responses and set TTLs. Cache stampede needs separate guards.
NestJS performance tips?
Same rules apply. Watch interceptors and class serialization - prefer explicit DTOs and Fastify adapter for hot paths.
When to migrate off Express?
When Flame shows framework overhead is small vs your sync handler work, fix handlers first. When Express limits are hit after tuning, see Fastify ADR.
Related
- Performance Basics - introductory examples
- Load Testing - k6 scenarios
- clinic.js Suite - profiling tools
- Event Loop Best Practices - non-blocking patterns
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.