arthrop0d

Brief articles on varied topics.

How Tail-Based Trace Sampling Works

observabilitydistributed systems

Tail-based trace sampling is a strategy in which a collector waits until it has enough of a distributed trace to judge it, then keeps or discards the trace as a whole. Instead of deciding when the first span is created, it decides at the trace’s “tail,” after the result, duration, errors, and other details are visible.

The problem it solves

A trace is the connected record of one operation as it moves through services. Its individual records are spans: one span might represent an API request, another a database query, and others work performed by downstream services. Spans belonging to the same trace share a trace ID.

Without tail-based sampling, systems commonly use head-based sampling: the first service involved in a request makes a keep-or-discard decision, and that decision is carried through the request. This is cheap and consistent, but the first service does not yet know how the operation will end.

A request that looks ordinary at its start might later return an HTTP 500, take 12 seconds, or trigger a retry storm. If the initial decision was “discard,” the evidence needed to diagnose that failure was never retained. Conversely, keeping every trace that starts normally would produce too much telemetry and cost too much to store.

Tail-based sampling lets you express decisions such as:

The important point is that these rules can inspect the trace’s outcome rather than guessing from its beginning.

The collector becomes a temporary store

A tail-sampling collector receives spans as services finish their work. It groups them by trace ID and holds them in memory or another temporary buffer instead of exporting each span immediately.

For example, a request might produce this sequence:

  1. The frontend creates a server span.
  2. The frontend calls an orders service.
  3. The orders service queries a database.
  4. A payment service returns an error.
  5. The frontend returns a failed response.

The payment error and the final response may arrive after the frontend’s first span. A collector that waits can see all of these facts together.

There is rarely a perfect signal that a trace is complete. Distributed systems can send spans out of order, and a service may finish one span while another service is still working. Therefore, the collector normally uses a decision wait period: it waits for a configured time after receiving trace data, allowing late spans to arrive. It then evaluates what it has.

Some implementations can also use an explicit end-of-trace signal or information about the root operation, but a quiet-period timeout remains necessary for missing, delayed, or reordered data. The wait is a trade-off: a longer period gives the collector more evidence but consumes more memory and delays export.

Making the decision from the whole trace

After the wait period, the collector evaluates sampling policies against the buffered spans. A policy might inspect span status, duration, service name, route, or attributes such as a customer tier. The policies can be combined: an error policy can take priority, while a rate policy limits the volume of successful traces.

The result is a trace-level decision, commonly one of these:

A policy should not keep only the span that matched. If a database span shows an error, the useful context includes the incoming request, its parent-child relationships, and the downstream calls around it. Tail sampling is valuable precisely because it makes the decision using the complete trace and applies it to the complete trace.

The collector is not making an independent decision for each span. It is deciding the fate of a group identified by one trace ID.

Keeping the decision consistent

Large deployments usually run multiple collector instances. A trace’s spans can arrive at different instances unless the receiving layer routes all spans with the same trace ID to the same place. Tail-sampling deployments therefore commonly use trace-aware routing—often called trace ID affinity—so one collector owns the complete buffered trace.

If spans are split across collectors, each collector sees only part of the operation. One might see a successful frontend span while another sees the payment failure, defeating the purpose of tail sampling. A routing layer can calculate a partition from the trace ID and send every span in that trace to the same collector. The collector may also need enough capacity and a bounded buffer to prevent one unusually large trace from exhausting resources.

Once the decision is made, the collector exports every retained span for a kept trace and exports none for a dropped trace. This is the consistency guarantee users expect when opening a trace in an observability system: its service calls and database work should not randomly disappear because each span was sampled separately.

Sampling metadata can also be carried in trace context—the small set of values propagated between services with a request. That metadata can influence downstream instrumentation and collectors, but it cannot recover spans that an earlier component already chose not to record. Tail sampling works best when the collector receives unsampled spans from all relevant services, at least for the traffic it is responsible for.

Costs and familiar configuration clues

Tail sampling spends resources to gain better evidence. The collector needs memory for in-flight traces, CPU to evaluate policies, and a timeout long enough to cover normal distributed latency. It also introduces export delay. If the buffer fills, the collector must evict data or apply backpressure, and some traces may be incomplete.

This is why configuration often contains settings resembling decision wait, maximum trace size, policy lists, and trace-aware load balancing. An incident report saying “the trace was sampled out” may mean the trace never matched a keep policy, while “partial trace” can indicate late spans, routing errors, or spans dropped before reaching the tail-sampling collector.

Tail-based sampling is therefore not simply “sample later.” It is a buffering and routing design: collect related spans, wait for the trace’s likely outcome, evaluate policies with that evidence, and then make one consistent keep-or-discard decision for the trace.

← All articles