Skip to content

Performance Diagnosis ​

Optimize the dominant constraint under a representative workload.

Define the experiment ​

Record:

  • route/message/job and payload distribution;
  • concurrency and arrival pattern;
  • warm-up and test duration;
  • data volume and cache state;
  • p50/p95/p99 latency, throughput, and errors;
  • CPU, event-loop delay/utilization, heap, GC, and RSS;
  • database query/pool/lock metrics;
  • external dependency spans;
  • queue depth, age, and processing duration.

Change one major variable at a time. Compare the same workload and environment.

Event loop and CPU ​

Node.js serves many clients with a small number of threads. A long synchronous callback delays unrelated requests and can become a denial-of-service vector when input controls its cost.

Inspect for:

  • synchronous filesystem/crypto/compression calls in request paths;
  • large JSON parse/stringify or validation;
  • unbounded loops, regex backtracking, sorting, and transformations;
  • image/PDF/media processing;
  • expensive logging or stack creation;
  • synchronous password hashing or encryption misuse.

Remedies:

  • bound input and algorithmic work;
  • use asynchronous APIs for I/O;
  • stream or partition large work where appropriate;
  • move CPU-heavy work to a pooled worker thread or separate worker process;
  • queue work when the result may be asynchronous and durable.

Worker threads help CPU-intensive JavaScript; they usually do not improve ordinary asynchronous I/O. Use a pool instead of spawning one worker per request.

Provider scope ​

Singleton is the default and preferred scope. Request-scoped providers are recreated for every request, and scope bubbles to their consumers.

Measure before using request scope broadly. Prefer explicit principals/context or an existing request-context mechanism when it keeps the graph singleton-safe. Never store per-request data in singleton fields.

Framework loading and lifecycle ​

Do not present lazy-loaded modules or lifecycle hooks as generic performance optimizations.

  • Lifecycle hooks coordinate initialization and shutdown correctness. Keep startup work bounded and observable, but optimize it only when startup measurements and deployment requirements identify a problem.
  • Nest lazy-loading support is useful for niche, optional dependency graphs. It adds wiring and first-use complexity and does not automatically reduce steady-state request latency or memory.
  • Dynamic imports, deferred initialization, and lazy modules can shift cost to the first request. Measure cold start, first-use latency, readiness, memory, and failure behavior before and after.
  • Avoid discovering dependencies at runtime merely to defer construction; preserve explicit provider contracts and verify the installed NestJS API.

Database ​

Database latency often dominates framework overhead.

Check:

  • N+1 query patterns;
  • missing or unused indexes for real predicates/order;
  • unbounded results and offset pagination at large depth;
  • overfetching wide rows/relations;
  • pool waits, leaked connections, and mismatched pool size;
  • long transactions and lock contention;
  • read-then-write races and missing constraints;
  • repeated counts/aggregations on hot paths.

Use query plans and production-like data. An index accelerates some reads while adding write/storage cost; do not add one from column names alone.

Keep external network calls outside database transactions whenever correctness permits.

External calls ​

  • Set a deadline/timeout for every dependency call.
  • Reuse connections/agents according to the client library.
  • Propagate cancellation where supported.
  • Retry only transient, safe operations within a total time budget.
  • Bound concurrency to protect both systems.
  • Use circuit breaking or load shedding only with explicit recovery behavior.

Parallelize independent calls only when it reduces latency without overwhelming downstream capacity.

Memory and payloads ​

Look for retained request data, unbounded maps/listeners, large buffers, ORM relation graphs, and full in-memory exports.

Use heap snapshots/allocation profiling to identify retainers. Stream or page large data; remove listeners/timers; bound caches. Increasing the heap may delay failure but does not repair a leak.

Caching ​

Cache only after identifying repeated expensive work and acceptable staleness. Measure hit ratio, load time, eviction, memory, and origin impact.

Protect against stampedes with single-flight/locking, early refresh, or jittered TTL where appropriate. Include tenant, locale, permissions, and representation in keys when they change the result.

Express versus Fastify ​

Fastify can improve HTTP-layer throughput, but application latency may be dominated by database or network work. Before migration:

  1. profile and isolate HTTP adapter overhead;
  2. inventory Express-specific middleware and response APIs;
  3. benchmark representative routes, validation, serialization, and logging;
  4. run all transport/e2e tests;
  5. verify deployment binding and proxy behavior.

Do not quote generic framework benchmarks as proof for this application.

Logging and observability overhead ​

Use structured logs with appropriate levels and redaction. Avoid serializing complete bodies, ORM entities, buffers, or stack traces on every successful request. Sample high-volume traces deliberately while retaining errors and critical transactions.

Result format ​

text
Baseline: workload + p95/p99 + throughput + errors
Bottleneck: evidence and dominant span/resource
Change: one intervention
Result: same metrics after change
Correctness: tests and failure behavior
Capacity: new saturation point and next limiting dependency

See Node.js guidance on event-loop safety and worker threads, plus NestJS guidance on injection scope and Fastify.


Canonical source: skills/nestjs-features-performance/references/performance-diagnosis.md. This page is generated during the documentation build.

Open-source guidance for deliberate NestJS engineering.