ObservabilityXLIV · SamplingSampling
Head vs Tail Sampling
What you'll learn
- Distinguish head sampling from tail sampling in production terms
- Explain when the sampling decision can be made at the producer and when it cannot
- Configure the probabilistic_sampler and tail_sampling processors in the OTel Collector
- Identify the cost trade-off between head and tail sampling for a given workload
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The checkout team investigates a regression. The fix shipped at 14:00. The post-mortem needs the trace of the one customer whose cart failed at 14:03. The on-call engineer opens Tempo. The backend holds traces sampled at one percent; the trace is not there. The investigation reverts to log correlation and best guessing, and the post-mortem is filed with “unable to reproduce” under evidence.
The sampling decision was made at the producer. The producer did not know the request would fail. The trace was discarded before its outcome was visible. A decision made at the producer is a head sample. A decision deferred until the full trace is visible is a tail sample. The location of the decision is the choice this lesson is about.
What it is
Head sampling makes the keep-or-drop decision at the producer, before the trace is complete. The decision is taken from inputs that exist at request entry: the trace identifier, a hash of a service attribute, a configured rate, a per-route flag. The producer then either ships the whole trace or ships nothing.
Tail sampling makes the keep-or-drop decision at a gateway collector, after the full trace has been buffered. The gateway sees the outcome of every span and applies a policy: keep all errors, keep traces over a latency threshold, sample five percent of the rest. The buffered spans of rejected traces are discarded; the spans of accepted traces are exported.
The two decisions live at different points in the pipeline.
HEAD SAMPLING
producer (SDK / edge collector)
|
| decision: keep? (hash + rate)
|
+-- drop --> [no further work]
|
+-- keep --> OTLP --> gateway --> backend
TAIL SAMPLING
producer (SDK / edge collector)
|
| decision: ship everything (or cheap head sample)
|
v
gateway collector
|
| buffer full trace until decision_wait expires
|
| apply policy: errors / latency / rate
|
+-- drop --> spans discarded
|
+-- keep --> backend
Why a sysadmin cares
Two failure shapes dominate sampling incidents. Both are about location of decision.
- The blind incident. A one-percent head sample is fine while the service is healthy: a randomly chosen one percent reproduces the population. The moment a specific failure mode appears, the sample no longer reproduces it. A rare error at one in ten thousand exists in roughly one in a million sampled traces. The platform team has a trace of the error only if the failure happened to be sampled. Most of the time it was not. The investigation ends with no evidence.
- The buffered gateway that ran out of memory. A team turns on tail sampling to keep all errors. The gateway now receives every span of every trace and buffers until the trace is complete. A traffic spike at five times normal fills the buffer. The collector is OOM-killed; traces are dropped wholesale, including the errors the team turned on tail sampling to keep.
Both failures are predictable. The fix in the first case is tail sampling on a policy that keeps errors. The fix in the second case is capacity for the buffer and a per-collector volume ceiling. The lesson returns to each.
How it works
The mental model is straightforward once the trace is understood as a tree of spans with a shared trace identifier.
A trace identifier is a 128-bit number. The OTel SDK attaches it to every span. The number is generated at the entry point and propagated to every downstream service. Every span of a given trace carries the same identifier.
Head sampling computes a hash of the trace identifier, takes the low-order bits, and compares them against a rate. A five percent sample keeps traces whose low-order bits land in the lowest five percent of the value range. The decision is the same for every service that hashes the same identifier, so a head sample applied at the producer agrees with a head sample applied at an edge collector, provided both use the same hash and the same seed.
Tail sampling makes no decision at the producer. Every span
arrives at the gateway. The gateway holds the spans in memory
until either the trace is complete (no more spans arrive for the
trace identifier within the decision window) or the configured
decision_wait expires. At that point the gateway evaluates the
policies against the buffered trace and either drops it or
exports it.
The decision point changes what information is available. At producer entry, no span has run; the outcome is unknown. At gateway completion, every span has run; the outcome is known. Head sampling cannot keep errors because it cannot see them. Tail sampling can keep errors because it can.
Trace 7a2f... at 0.05 sampling (head)
producer entry: hash(7a2f...) mod 100 = 17
17 not in [0..5) -> drop
(the request was an error; the trace is gone)
Trace 8c4e... at full rate into tail sampler (tail)
producer entry: ship everything
gateway: buffer 14 spans for 10s (decision_wait)
gateway: status_code = ERROR on root span
gateway: policy: status_code ERROR -> keep
(the error is exported)
Under the hood
How to configure it
A producer-side head sample uses the probabilistic_sampler
processor. The decision is per-trace; consistent hashing on the
trace identifier means every service agrees.
# /etc/otelcol/config.yaml (edge collector, head sampling)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
# Head sample at 5 percent. The hash_seed is the same across
# every collector in the fleet; if it differs, the sample
# population differs.
probabilistic_sampler:
sampling_percentage: 5
hash_seed: 42
batch:
timeout: 5s
send_batch_size: 8192
exporters:
otlp:
endpoint: gateway.observability.internal:4317
tls:
insecure: false
ca_file: /etc/ssl/certs/ca-certificates.crt
service:
pipelines:
traces:
receivers: [otlp]
processors: [probabilistic_sampler, batch]
exporters: [otlp]
A gateway-side tail sample uses the tail_sampling processor.
The producer no longer samples; it ships every span. The gateway
decides.
# /etc/otelcol/config.yaml (gateway collector, tail sampling)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
# Bounds the in-memory trace map. A value too small silently
# drops traces when the map fills.
tail_sampling:
decision_wait: 10s
num_traces: 50000
expected_new_traces_per_sec: 1000
policies:
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
- name: keep-slow
type: latency
latency:
threshold_ms: 1000
- name: keep-baseline
type: probabilistic
probabilistic:
sampling_percentage: 5
batch:
timeout: 5s
send_batch_size: 8192
exporters:
otlp:
endpoint: tempo.observability.internal:4317
sending_queue:
enabled: true
queue_size: 5000
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlp]
The two configs are not exclusive. A fleet can run an edge
collector with probabilistic_sampler to drop ninety-five
percent of bulk traffic, and a gateway collector with
tail_sampling to apply policy to the five percent that
survived. The combined pipeline is the production pattern; the
next lesson names it.
How to validate it
Validation confirms the right processor is wired and that the metrics show the expected decisions.
# CONFIGURATION: parse-check the collector config.
otelcol validate --config=/etc/otelcol/config.yaml
# (no output on success; non-zero exit on error)
# READ-ONLY: confirm the binary ships both processors.
otelcol components | grep -E 'probabilistic_sampler|tail_sampling'
- name: probabilistic_sampler
type: processor
- name: tail_sampling
type: processor
# READ-ONLY: read the probabilistic sampler metrics.
curl -s http://localhost:8888/metrics | grep probabilistic_sampler
otelcol_processor_probabilistic_sampler_count_traces_sampled{policy="probs"} 1287
otelcol_processor_probabilistic_sampler_count_traces_dropped{policy="probs"} 24018
A count_traces_sampled to count_traces_dropped ratio close
to the configured percentage is the expected steady state. A
ratio wildly different from the configuration means the
sampling_percentage is not what was intended or the
hash_seed does not match the fleet.
# READ-ONLY: read the tail sampler counters.
curl -s http://localhost:8888/metrics | grep tail_sampling
otelcol_processor_tail_sampling_count_traces_kept{policy="keep-errors"} 42
otelcol_processor_tail_sampling_count_traces_kept{policy="keep-slow"} 17
otelcol_processor_tail_sampling_count_traces_kept{policy="keep-baseline"} 1230
otelcol_processor_tail_sampling_count_traces_dropped 24018
A trace dropped by the tail sampler never reaches Tempo. If the
dropped counter climbs while the kept counters are flat, the
trace map (num_traces) may be saturated or the decision_wait
may be expiring before all spans arrive.
How it can fail
Five failure modes specific to the head-versus-tail decision.
- Head sampling on a service that needs to keep errors. A checkout service runs at five percent head sampling. A regression introduces a one-in-ten-thousand error. The sampled population contains roughly one error per two million traces. Symptom: Tempo returns no traces of the error during the incident; the post-mortem has no evidence; the regression repeats.
- Tail sampling without enough
num_traces. A gateway collector is configured withnum_traces: 5000against a real volume of 50 000 in-flight traces. Symptom: the in-memory map fills; the processor logsforced to drop; the dropped traces include the very errors the policy was written to keep. - Decision window shorter than the slowest trace. A
decision_wait: 1sis set against a backend whose p99 trace duration is 8 seconds. Symptom: spans still arrive after the decision is taken; the kept trace is incomplete; the dropped trace is silent; the policy outcome is decided on a partial trace. - Inconsistent hash seed between edge collectors. One edge
collector uses
hash_seed: 42, another useshash_seed: 7. Symptom: the same trace identifier is sampled by one edge collector and dropped by another; the trace arrives at the backend with missing spans; the latency picture is incomplete. - Tail sampler placed before the policy processor. The
tail_samplingprocessor runs before aresourceprocessor that would add theenvlabel. Symptom: the policies cannot filter onenv; the same policy applies to production and staging; staging traffic floods the production backend. - Head sample on a service that has no fallback. A service runs at one percent head sampling with no tail sampler downstream. Symptom: a one-off bug that affects fewer than one in a hundred requests produces zero retained traces; the on-call engineer has nothing to investigate.
How to troubleshoot it
A missing-trace investigation asks three questions in order.
- Is the producer sampling? Check
otelcol_processor_probabilistic_sampler_count_traces_sampledon the edge collector. If the counter is climbing, the producer is sampling. If the counter is flat and the receiver is accepting, the producer config is not loaded. - Is the gateway tail sampling? Check
otelcol_processor_tail_sampling_count_traces_keptper policy. If every policy counter is flat while the receiver is accepting spans, the tail sampler config is not loaded or theservice.pipelines.traces.processorsdoes not includetail_sampling. - Is the decision window long enough? Compare the slowest
trace p99 to
decision_wait. If the trace p99 is greater thandecision_wait, the decision is being made on incomplete traces. Increasedecision_waitto roughly twice the p99 and re-validate. - Is the in-memory map filling? Watch
otelcol_processor_tail_sampling_traces_dropped_too_earlyand the trace count metric on the processor. If either climbs, raisenum_tracesor add additional gateway collectors.
Security implications
- Trace contents. Tail sampling keeps every span until the
decision window expires. Sensitive fields (PII, secrets, query
parameters) sit in memory for
decision_waitplus a buffer. Shorter windows reduce the exposure; longer windows give the policy time to evaluate. The trade-off is between privacy and completeness. - Hash seed. The
hash_seedof aprobabilistic_samplerdetermines which trace identifiers are sampled. If the seed is predictable and an attacker can influence trace identifiers, they can choose whether their traffic is sampled or not. A random seed chosen at deploy time and not reused across distinct security domains is the production pattern. - PII in span attributes. Tail sampling with a
string_attributepolicy can keep traces whose spans contain customer identifiers. The export then carries the identifiers into Tempo. The downstream retention and redaction rules apply.
Performance implications
- Head sampling cost. The
probabilistic_samplerprocessor is constant-time per span. It does not buffer; it does not grow with traffic. The cost is the cost of a hash. A producer running at 10 000 spans per second spends a fraction of a CPU core on the decision. - Tail sampling cost. The
tail_samplingprocessor grows with the in-memory trace map. A rough ceiling isnum_traces × average_trace_size_bytes; fifty thousand traces at an average two hundred kilobytes is ten gigabytes of resident memory. The CPU cost is dominated by policy evaluation on every decision. A gateway running tail sampling is a single-purpose host in most production deployments. - Network. Head sampling reduces network egress at the producer: dropped traces never leave the host. Tail sampling increases network ingress at the gateway: every span arrives before the decision. The trade-off is between edge bandwidth and gateway bandwidth.
Production guidance
- Pick the location per service. Critical-path services (auth, payments, search) get tail sampling at the gateway. Bulk services (telemetry, internal cron, background workers) get head sampling at the producer.
- Pin the binary. The
tail_samplingprocessor is inotelcol-contrib. A core-binary deployment will fail at startup. Runotelcol componentsand confirm. - Hash seed consistency. Every edge collector in the fleet
must use the same
hash_seed. A drifted seed changes which traces are sampled and breaks correlation between services. - Right-size
num_traces. Setnum_tracesto roughlyexpected_new_traces_per_sec × decision_wait × 2. The factor of two is for trace-rate variance. Watchotelcol_processor_tail_sampling_traces_dropped_too_earlyto detect under-sizing.
Verification
You should now be able to answer:
- What is the operational difference between head sampling and tail sampling?
- Why does the producer not have enough information to make a tail-style decision?
- What is the cost trade-off when tail sampling is enabled?
- Which processor (
probabilistic_samplerortail_sampling) lives at the gateway, and why?
Quiz
Knowledge check · 8 questions
Q1. A tail sampler decides whether to keep a trace:
Q2. Head sampling cannot keep rare errors because:
Q3. A consistent hash_seed on every edge collector ensures the same trace identifier is sampled (or dropped) across the fleet.
Q4. The tail_sampling processor lives at:
Q5. Name the OTel Collector processor that performs head sampling.
Q6. Which of these are valid signals that head-versus-tail sampling is configured correctly?
Q7. A team enables tail sampling but the binary was built from otelcol (core), not otelcol-contrib. The collector will:
Q8. A 1 percent head sample is sufficient for:
Passing score: 75%. Answers are checked in this browser.