Skip to main content
ApiTraffic’s APM answers the question a request timeline cannot: where did those 900 ms actually go? Alongside the request and response it already captures, the SDK records the work your application did while handling the request — database queries, cache calls, outbound HTTP, queue publishes, RPC and GraphQL — as spans, and stores them with the request.

Enable APM

Install the bridge and start capturing spans

Capture Settings

SQL values, Redis keys and code location — off by default

Performance Analytics

Find the query or connection that is costing you

APM API

Query the same data programmatically

Spans

A span is one unit of work inside a request: a SELECT, a GET to Redis, a call to Stripe. Every span carries the type of work, a name, when it started relative to the request, how long it took, whether it succeeded, and a bag of metadata from the instrumentation that produced it. Spans are captured by OpenTelemetry auto-instrumentation — you do not annotate your code. Outbound HTTP over http/https and global fetch is captured by the base client instead, which is the path that records full request and response bodies; see Outbound Requests for what that covers and what it cannot see.

Attribution

The subtle part of APM in Node.js is deciding which request a span belongs to. A pg query finishes inside a pool callback, and an outbound response fires on a keep-alive socket that may be shared with an entirely different request — so by the time a span ends, the async context is no longer the caller’s. ApiTraffic therefore captures the owning request when the operation starts and holds it until the operation completes. That applies to OpenTelemetry spans, to the native outbound hook (the context is taken synchronously at http.request()), and to the framework middlewares, which hold their request instance rather than looking it up again when the response is sent.
If a request that clearly performs queries shows no waterfall, the usual cause is that APM was never registered in that process, or that it was registered after the drivers were required. See troubleshooting.
Operations that fail are recorded too — a connection refused or a timeout produces a failed span rather than a gap in the timeline.

Fingerprints

Every span is fingerprinted so that the same logical operation collapses to a single identity no matter what values it ran with. SELECT * FROM users WHERE id = 42 and ... id = 91 normalize to the same statement and therefore the same fingerprint, and Redis keys are masked to their shape (user:*:profile) before fingerprinting. Fingerprints are always computed from the normalized form, so turning value capture on or off never splits an operation’s history in the analytics.

Per-request rollups

As spans are recorded, the SDK also accumulates per-request totals — time and count for database, Redis, cache, outbound HTTP, queue and RPC work — and sends them on the request itself. These rollups are what the dashboards read, which is why they stay fast regardless of how many spans you produce; the span store is only read when you drill into a single operation. Time not attributed to any dependency is reported as app time: the request duration minus the instrumented families, floored at zero. Because dependency work can overlap (concurrent queries), a heavily parallel request can show 0 ms of app time.

Distributed traces

When a request arrives with a W3C traceparent header, ApiTraffic records its trace id on the request; when it does not, the trace id from the first span is used. Requests that share a trace id are grouped in the Distributed Trace tab of a request, so a call that crosses several of your services reads as one timeline. The trace id is taken from the original headers, before redaction, so redacting traceparent does not break trace grouping.

Overhead and limits

APM is designed to be safe to leave on in production.
  • Fail-open. The bridge never throws into your code, never alters a wrapped call and never blocks the request path. On an internal error the span is dropped.
  • Bounded per request. At most maxSpansPerRequest spans (default 500) are retained; overflow is counted, not buffered. Span metadata is capped at maxSpanMetaBytes (default 2048) per span.
  • Sampling. spanSampleRate retains span detail for a fraction of requests. Rollups are still counted for every request, so the dashboards remain accurate while the span store shrinks.
  • No values by default. Statements are normalized and bind values discarded in-process, before anything leaves your server. See capture settings.