Topic 42

Sampling

Sampling

Tracing every request is the one part of this stack that genuinely doesn't scale by default. At Harborline's summer-Saturday peak, search alone takes 300 requests per second, each producing 8–12 spans — over 200 million spans a day from one service, almost all of them describing identical healthy cache hits that nobody will ever look up.

Sampling is the deliberate decision to keep some traces and drop the rest, and the entire craft is which ones. This is the second of the book's three costs from Chapter 1, applied to the most expensive signal — and applied badly, it deletes exactly the traces this chapter exists to capture.

The Bill for Completeness

The arithmetic deserves to be explicit: spans per request, times requests per second, times bytes per span, compounded across five services. Storage is actually the smaller line item — export bandwidth off the hosts, Collector CPU at the gateway, and query-time noise all scale with the traces you keep. And 99% of what a full retention buys is redundant: the ten-thousandth healthy 40 ms search trace teaches nothing the first one didn't.

Head Sampling

Head sampling makes the decision at the root span's birth, inside the SDK, before anything about the request is known. ParentBased(TraceIdRatioBased(0.10)) keeps 10% of new traces, and the ParentBased wrapper makes every downstream service honor the sampled bit arriving in the traceparent flags byte (Topic 39) instead of re-rolling the dice. The root decides once; bookings, payments, and notifier all follow. Cheap, stateless — and blind.

The Blindness, Precisely

Head sampling decides before the outcome exists, so at 10% it discards 90% of the errors and 90% of the slow requests right along with the boring traffic — the sampler cannot favor what it cannot yet see. Harborline's Saturday mystery fires a few dozen slow checkouts an hour; a 10% head sample would have captured a handful, and a cost-driven 1% would likely have captured none. This is the observability-native footgun: sampling away exactly the requests you built tracing to see.

Tail Sampling

Tail sampling moves the decision to the end of the trace, into the Collector gateway. The tail_sampling processor — contrib distribution, as Topic 41 flagged — buffers each trace for a decision_wait window of 10–30 seconds, then applies policies top to bottom, now knowing the trace's duration and status. Harborline's policy keeps every trace with an error status, keeps every trace slower than 500 ms — the checkout SLO threshold, reused deliberately — and takes a probabilistic 5% of the healthy rest.

processors:
  tail_sampling:
    decision_wait: 30s
    policies:
      - name: keep-errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: keep-slow
        type: latency
        latency: { threshold_ms: 500 }
      - name: keep-baseline
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }

Read the policy as keep-rules, in words: 100% of failures, 100% of SLO breaches, a thin residue of healthy baseline for comparison. The interesting traces at Harborline are rare by definition — that is what makes them interesting — and this is the mechanism that guarantees the Saturday trace exists in Topic 43 instead of surviving a dice roll.

Head vs tail sampling — where the decision is made, and what it can see
Head sampling
Decides at the root span's birth, in the SDK, before any outcome exists. Cheap and stateless — but blind: at 10% it drops 90% of errors along with the boring traffic.
Tail sampling
Decides after the trace completes, in the Collector gateway, with duration and status known. Keeps 100% of errors and every trace over the 500 ms SLO; costs buffering memory and trace-to-instance affinity.

What Tail Sampling Costs

The trade is memory and topology. The gateway holds every in-flight trace for the full decision window, which is real memory at peak load — and every span of one trace must reach the same Collector instance, or each instance sees a fragment and decides on incomplete evidence. With Harborline's single gateway on obs-01 the affinity is free. A scaled-out gateway tier needs the load-balancing exporter in front, routing spans by trace ID, before tail_sampling can be trusted.

Composing a Policy

Head and tail stack. A head sample in the SDK caps what leaves the hosts at all — search at Saturday peak is the service that needs it — and tail policies at the gateway spend the surviving budget on errors and outliers. One caveat travels with any sampling at all: kept traces can no longer answer "how many." After keep-all-errors sampling, failures are overrepresented by design, so rates and ratios stay the metrics' job, and traces answer where and why.

Head vs Tail Sampling

Head sampling — decides at the trace's start, in the SDK, with zero knowledge of the outcome: no buffering, no topology constraints, and it cuts export cost at the source. Choose it as a cheap first-stage cap on healthy high-volume traffic.

Tail sampling — decides after the trace completes, in the Collector, seeing duration and status: it keeps 100% of errors and slow traces while sampling the boring 99%, at the price of buffering memory and trace-to-instance affinity. Choose it wherever the interesting traces are rare — which is every debugging scenario this book cares about.

Common Mistakes
  • Setting TraceIdRatioBased(0.01) fleet-wide to control cost — the one-in-ten-thousand error trace now survives with 1% probability, and the incident review finds four spans where the evidence should be.
  • Using a plain TraceIdRatioBased sampler without ParentBased in downstream services — each service re-rolls the dice, bookings keeps a trace that payments dropped, and the backend fills with half-traces that look like propagation bugs.
  • Scaling the gateway to two instances behind a round-robin load balancer with tail_sampling enabled — spans of one trace split across instances, each sees a partial trace, and policy decisions become coin flips; the load-balancing exporter routing by trace ID is the prerequisite.
  • Setting decision_wait to 5 s to save gateway memory — Harborline's slowest checkouts run 9+ seconds, so the traces the latency policy exists to keep get evaluated before their spans finish arriving, and the policy keeps fragments.
  • Reporting error rates from sampled traces — after keep-all-errors tail sampling, errors are overrepresented by design; rates and ratios come from Prometheus counters, traces come sampled.
Best Practices
  • Write the tail policy as keep-rules first: 100% of status_code = ERROR, 100% of latency over the SLO threshold (500 ms for checkout), then a probabilistic residue of 5–10% for healthy-baseline comparison traces.
  • Use ParentBased samplers in every service so the root's decision propagates and traces arrive whole or not at all.
  • Size decision_wait past the p99.9 of trace duration — a policy that decides before the slow traces finish arriving is a policy that samples them away.
  • Alert on the sampling pipeline itself via the Collector's own metrics — a misconfigured policy silently dropping everything looks identical to a healthy quiet system until the day you need a trace.
Comparable toolsJaeger Remote sampling — per-service head-sampling configs served from the backendHoneycomb Refinery — a dedicated tail-sampling proxy, the concept's best-known standalone implementationDatadog APM — head sampling in the agent plus retention filters approximating tail behaviorAWS X-Ray — centrally managed rules with a reservoir-plus-rate model

Knowledge Check

Why does a 10% head sample discard 90% of error traces, not just 90% of boring ones?

  • The sampler preferentially drops errors to reduce noise
  • The decision is made before the outcome exists, so errors get no special treatment
  • Error spans fail to export because the request crashed
  • ParentBased overrides the sampling ratio specifically for failed and errored requests

Why does tail_sampling require all spans of a trace to reach the same Collector instance?

  • OTLP connections are pinned per trace at the protocol level
  • Splitting traces would double the gateway's memory use
  • The policy evaluates the whole trace, so a partial view produces wrong decisions
  • Tempo can only ingest a trace from one source

What does wrapping the sampler in ParentBased guarantee across Harborline's five services?

  • One decision at the root, honored by every downstream service
  • A higher overall sampling rate than the configured ratio
  • That error traces are always kept regardless of the ratio
  • That traceparent headers are injected into every outbound call

After deploying keep-all-errors tail sampling, where should Harborline's checkout error rate come from?

  • Counting error traces vs healthy traces in Tempo
  • The 5% probabilistic residue, scaled up by 20
  • The Collector's otelcol_* pipeline metrics
  • Prometheus counters, which count every request

Why does Harborline run decision_wait at 30 s instead of a snappier 5 s, and what does that trade?

  • Export bandwidth for faster queries
  • Gateway memory for complete slow traces — checkouts run 9+ seconds
  • A lower sampling rate for a higher one
  • Instance affinity for round-robin load balancing across gateway replicas

You got correct