ELU as a first-class metric, clinic.js toolchain, V8 CPU profiles, core dump analysis, and distributed tracing across Kafka-connected services.
Module 12 — Production Observability, Performance Profiling & Flame Graphs
What this module covers: A blockchain indexer at 50,000 events/second produces hundreds of thousands of function calls per second. When performance degrades, the cause is buried in microseconds — one function accounting for 40% of CPU, one allocation pattern triggering GC every 200ms, one async chain adding 15ms of hidden latency per request. Structured logs, Prometheus metrics, and distributed traces tell you that something is wrong. V8 CPU profiles, flame graphs, and the clinic.js toolchain tell you exactly where and why. This module covers the complete production diagnostic stack for Node.js systems under high-throughput stress.
The Three Pillars: Logs, Metrics, Traces
Every production Node.js service needs all three. They answer different questions:
- Logs: what happened? (event-level detail)
- Metrics: how is the system behaving over time? (aggregate measurements)
- Traces: how does a single request flow through the system? (distributed causality)
Pino: The Fastest Structured Logger
Why Pino over Winston at high throughput: Pino defers JSON serialization to a separate worker thread. At 50K events/second where each event emits 2–3 log entries: 100K log statements/sec. Winston's synchronous JSON serialization adds ~2ms per 100K logs. Pino's worker thread adds ~0.1ms. At scale, logging becomes a measurable CPU cost.
Prometheus Metrics with prom-client
Key metrics for a blockchain indexer:
| Metric | Type | Alert Condition |
|---|---|---|
nodejs_event_loop_utilization | Gauge | > 0.85 |
nodejs_gc_duration_seconds (P99) | Histogram | > 50ms |
transactions_ingested_total rate | Counter | Drop > 20% |
transaction_processing_duration_seconds P99 | Histogram | > 100ms |
db_pool_utilization_ratio | Gauge | > 0.9 |
nodejs_heap_used_bytes | Gauge | > 80% of max |
OpenTelemetry: Distributed Traces
With distributed tracing, a single block processing request shows its full latency breakdown across: HTTP receive → parse → signature verification (worker thread) → database write → Kafka publish. You can see exactly which step is slow without guessing.
Event Loop Utilization: The First Metric to Check
ELU was introduced in Module 2. In production, it is the first metric to check during a latency incident.
Generating V8 CPU Profiles
A CPU profile samples the call stack every N microseconds. The result: a list of functions and how many samples they appear in. Functions that appear in many samples are consuming CPU time.
Method 1: --cpu-prof Flag
Method 2: V8 Profiler API
Reading Flame Graphs
A flame graph visualizes CPU time as a stack of colored bars:
- X axis: total CPU time (wider = more CPU)
- Y axis: call stack depth (higher = deeper in the call chain)
- Color: random, for visual distinction
- Width of a bar: proportion of CPU time spent in that function and its callees
Reading rules:
- Wide plateaus at the top are hot spots — functions that consume a large fraction of CPU
- Narrow spikes are expected call depths — not performance problems
- Look for width at the TOP of the stack — if
JSON.parseis 35% wide at the top level, it's consuming 35% of total CPU
Actionable patterns:
The clinic.js Diagnostic Toolchain
clinic.js is the most comprehensive Node.js diagnostic suite. Three tools, each answering a different question.
clinic doctor: What Is Wrong?
clinic doctor analyzes four signals:
- Event loop delay — is the event loop lagging? (indicates blocking code)
- CPU usage — is the process CPU-bound?
- Memory — is memory growing continuously? (indicates leak)
- Handles/requests — are there open handles preventing process exit? (indicates resource leak)
What the report tells you:
clinic flame: Where Is the CPU Going?
clinic flame generates a proper flamegraph from --perf profiling data, with:
- Merged V8 and native frames (you see both JS and C++ in one view)
- Click-to-zoom for deep inspection
- Filtering by function name
Interpreting clinic flame output for a transaction parser:
clinic bubbleprof: Where Is the Async Time Going?
bubbleprof visualizes async operations as bubbles — the size of each bubble represents how long async operations took. It shows where time is spent waiting rather than executing.
Useful for diagnosing:
- Database queries that are slower than expected
- HTTP client calls with unexpected latency
- Async chains that add unnecessary await depth
Heap Snapshots: Finding Memory Leaks
When clinic doctor shows continuously growing memory, take a heap snapshot:
Common memory leak patterns in Node.js:
auto_explain for Node.js: async_hooks + Performance Timing
The equivalent of PostgreSQL's auto_explain for Node.js: automatically log slow async operations above a threshold.
The Production Incident: Undetected Memory Leak via Event Listener
Context: A blockchain indexer WebSocket subscription service. Engineers received an alert: process memory at 94% of limit after 6 hours of operation. Restart fixed it temporarily.
Diagnosis with heap snapshot comparison:
Snapshot 1 (at startup): 180MB heap Snapshot 2 (6 hours later): 1.8GB heap
Chrome DevTools comparison view showed:
183,000 EventEmitter instances — one per WebSocket subscriber (12,000 subscribers × ~15 listeners each = 180,000 accumulated listener objects that were never cleaned up).
The broken code:
The fix:
After the fix: memory stabilized at 220MB regardless of subscriber count or runtime duration.
Summary
| Concept | Key Takeaway |
|---|---|
| Pino | 5x faster than Winston. Structured JSON logging. Use child loggers for request context. |
| Prometheus + prom-client | Counter/Histogram/Gauge. Export /metrics. Alert on ELU, GC duration, pool utilization. |
| OpenTelemetry | Auto-instruments HTTP, pg, Kafka. Use startSpan for custom instrumentation. |
| ELU gauge | First metric to check during latency incident. > 0.85 = investigate CPU hotspot. |
--cpu-prof | Generate V8 CPU profile. Open in Chrome DevTools. Identify hot functions. |
| Flame graph | X axis = CPU time. Wide plateau at top = hot spot. Narrow spike = expected depth. |
clinic doctor | Diagnoses event loop delay, memory leaks, CPU saturation. First tool in the diagnostic workflow. |
clinic flame | CPU flamegraph with V8 + native frames. Identifies exact function consuming CPU. |
clinic bubbleprof | Async operation visualization. Shows where time is waiting vs executing. |
| Heap snapshots | Two snapshots + comparison view = pinpoint leaked object type. kill -SIGUSR2 to trigger. |
| EventEmitter leak | Always off() listeners when the subscriber closes. Most common Node.js memory leak. |
You can now diagnose performance problems with precision. Module 13 covers the connection layer — the pool configurations and process management patterns that determine how many requests your service can actually handle concurrently.
Next: Module 13 — Advanced Connection Pooling & Process Management →
When analyzing a flame graph generated from a Node.js CPU profile, what does a "wide plateau" at the top of a stack represent?
Which diagnostic tool from the clinic.js toolchain is best suited for visualizing where an application is spending time *waiting* for asynchronous operations (like database queries or HTTP calls) rather than executing CPU instructions?
What is identified as one of the most common causes of memory leaks in Node.js applications?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion