> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apitraffic.io/llms.txt
> Use this file to discover all available pages before exploring further.

# How APM Works

> What ApiTraffic captures inside a request, how it is attributed, and what it costs

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](https://docs.apitraffic.io/apm/setup.md)**
>
> Install the bridge and start capturing spans

> **[Capture Settings](https://docs.apitraffic.io/apm/capture-settings.md)**
>
> SQL values, Redis keys and code location — off by default

> **[Performance Analytics](https://docs.apitraffic.io/apm/analytics.md)**
>
> Find the query or connection that is costing you

> **[APM API](https://docs.apitraffic.io/api-reference/apm.md)**
>
> 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.

| Type | What it covers |
| - | - |
| `db.query` | SQL databases, MongoDB, DynamoDB |
| `redis` | Redis / ioredis commands |
| `cache` | Memcached and other caches |
| `http.out` | Outbound HTTP calls, including `fetch`/`undici` (see [Outbound Requests](https://docs.apitraffic.io/outbound-requests.md)) |
| `queue` | Messaging systems (Kafka, RabbitMQ, SQS, SNS) |
| `rpc` | gRPC and other RPC |
| `graphql` | GraphQL operations and resolvers |
| `custom` | Spans you record yourself |

Spans are captured by [OpenTelemetry auto-instrumentation](https://docs.apitraffic.io/apm/setup.md) — 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](https://docs.apitraffic.io/outbound-requests.md) 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.

> [!NOTE]
>
> 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](https://docs.apitraffic.io/apm/setup.md#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](https://docs.apitraffic.io/apm/capture-settings.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.