Instrumenting Node.js with OpenTelemetry means adding the OpenTelemetry Node SDK and auto-instrumentation so your application emits traces, metrics and logs over OTLP with little or no code change..
Instrumenting Node.js with OpenTelemetry means adding the OpenTelemetry Node SDK and auto-instrumentation so your application emits traces, metrics and logs over OTLP with little or no code change.
A Node.js service has two failure surfaces, and they need different signals. The request surface is what your users feel: latency and error rate per route, and the latency of every downstream call the request makes to a database or another API. The runtime surface is unique to Node's single-threaded, event-loop model: if the event loop is blocked by synchronous CPU work, every in-flight request stalls at once, which no per-route metric alone will explain.
So a complete picture is per-route HTTP timing and errors, downstream span latency, and the Node runtime health, event-loop lag, heap usage against the limit, and garbage-collection pauses, plus unhandled exceptions and rejections that would otherwise crash or silently degrade the process.
Node.js has strong zero-code instrumentation. The @opentelemetry/auto-instrumentations-node package patches the common libraries, the HTTP server and client, Express or Fastify, gRPC, and popular database and cache clients, so every inbound request becomes a span and every downstream call a child span, with no edits to your application. You register it before your app loads, typically through Node's --require flag or NODE_OPTIONS, and point the OTLP exporter at your Collector.
Auto-instrumentation covers the request surface; the runtime surface needs the runtime-metrics instrumentation, which emits event-loop and heap and GC metrics from V8. Add manual spans only where your own business logic does work the libraries cannot see. The one detail that matters for correlation is context propagation: as long as the auto-instrumentation is active, the trace context flows across async boundaries and outbound calls, so a slow request links to the exact downstream span that caused it.
These are the signals that explain almost every Node.js incident:
| Signal | Source | What it tells you / watch for |
|---|---|---|
http.server.request.duration | HTTP instrumentation | Per-route latency histogram. Track p95/p99 by route; a single slow route rarely means the whole service is slow. |
error rate (http.server.request.duration count where status >= 500) | HTTP instrumentation | 5xx responses per route. Correlate with a downstream span or a recent deploy. |
nodejs.eventloop.utilization / event-loop lag | runtime instrumentation | The Node-specific tell. Rising lag means synchronous work is blocking the loop and stalling every request. |
v8js.memory.heap.used vs heap limit | runtime instrumentation | Heap climbing steadily toward the limit is a leak; the process will eventually crash or GC-thrash. |
v8js.gc.duration | runtime instrumentation | Long or frequent GC pauses freeze the event loop and show up as latency spikes. |
downstream span duration | client instrumentation | Latency of DB and API calls inside a request; usually where a slow request's time actually goes. |
A Node.js dashboard should let you decide in seconds whether a latency problem is in your code, in the runtime, or in a dependency. Row one is request health: p95 and p99 latency by route and 5xx rate by route. Row two is the runtime: event-loop lag, heap used against the limit, and GC pause time, the three panels that reveal blocking, leaks and GC pressure respectively. Row three is dependencies: p95 latency of outbound calls grouped by target, so a slow database or upstream API is obvious at a glance.
# p99 latency by route (find the slow endpoint, not just "the service")
histogram_quantile(0.99, http.server.request.duration) by (http.route)
# event-loop lag: the Node-specific blocking signal
nodejs.eventloop.utilization
# heap headroom: used / limit climbing toward 1.0 = leak
v8js.memory.heap.used / v8js.memory.heap.limit
Every route slows simultaneously while CPU sits high on one thread. The cause is synchronous work on the request path, a large JSON parse, sync crypto, a tight loop. Event-loop lag spikes while downstream spans look fine, which is how you tell blocking apart from a slow dependency. Move the work off the loop or into a worker thread.
Heap used climbs release over release and never fully drops after GC, ending in an out-of-memory crash or constant GC. Watch v8js.memory.heap.used trend across deploys; a monotonic climb is the signature. Common culprits are unbounded caches and listeners that are never removed.
One route's p99 rises and its downstream span, a specific query or API call, accounts for the added time. The runtime panels stay healthy. The fix lives in the dependency, not the Node process.
Reasonable starting thresholds to tune to your SLOs:
| Condition | Signal | Meaning |
|---|---|---|
| 5xx rate by route > 2% for 5m | errors | A route is failing, not just slow. |
| p99 http.server.request.duration > your SLO for 10m | latency | Sustained latency regression on a route. |
| event-loop lag > 100ms for 2m | runtime | The loop is blocked; requests are queueing. |
| heap used / limit > 0.9 for 10m | memory | Leak or undersized heap; crash risk. |
Ops Singularity's TelemetryOps ingests your Node.js OpenTelemetry data natively, and Sentinel AI resolves the service incidents it reveals through governed Action Tickets validated by Sherlock. Explore TelemetryOps and ServiceOps.
Install the OpenTelemetry Node SDK and @opentelemetry/auto-instrumentations-node, register it before your application loads, and configure the OTLP exporter to send traces and metrics to an OpenTelemetry Collector.
Yes. The auto-instrumentations-node package patches common libraries like HTTP, Express and popular database clients, so you get traces and metrics with little or no code change.
Bring a real incident. We will show you Sentinel investigate, act and verify end to end, with every action reversible and audited.