Debugging Traces That Never Reach the Backend
Switch one instance to the console exporter and issue a request: if spans print, the SDK is fine and the problem is endpoint, protocol, TLS, or network policy — and if nothing prints, no amount of pipeline debugging will help because the spans were never created.
Context and when it matters
“No traces from this service” has two completely different root causes that look identical from the trace UI, and the console exporter separates them in under a minute. Skipping that step is the most common reason these investigations take hours: people begin by checking the Collector, find it healthy, check the network, find it fine, and eventually discover the SDK never instrumented anything.
Once creation is confirmed, the export path has a small number of failure points, and each has a decisive test. This page walks them in the order that eliminates the most possibilities per step.
The export path
The tests, in order
1. Do the spans exist?
OTEL_TRACES_EXPORTER=console OTEL_LOG_LEVEL=debug ./service
# Issue one request, then look for span JSON on stdout.
Nothing printed means creation failed, and the causes are: the SDK initialized after the framework was imported, the sampler is effectively zero, the instrumentation package is missing, or — for forking servers — the provider lives in the master process rather than the worker. That last one is covered for Django in instrumenting Django with OpenTelemetry and applies equally to gunicorn, uWSGI, and Celery.
2. Do endpoint, protocol, and port agree?
env | grep OTEL_EXPORTER
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf ← mismatch: 4317 is gRPC
The three recurring mismatches: gRPC protocol against the HTTP port (4318) or the reverse (4317); an endpoint including /v1/traces when the SDK appends it, producing /v1/traces/v1/traces; and https:// against a plaintext receiver, which fails during TLS handshake with an error that mentions neither TLS nor tracing.
3. Can the pod reach the Collector?
kubectl exec -it deploy/checkout-api -- sh -c '
# An empty OTLP payload is enough to prove reachability — expect 200 or 400,
# both of which mean the receiver answered. Connection refused means it did not.
curl -sS -o /dev/null -w "%{http_code}\n" \
-X POST http://otel-collector:4318/v1/traces \
-H "Content-Type: application/json" -d "{}"'
A hang rather than a refusal usually means a NetworkPolicy is dropping the packets silently, which is a different fix from a wrong hostname and is worth distinguishing before anyone edits the deployment.
4. Is the Collector accepting them?
curl -s http://otel-collector:8888/metrics \
| grep -E 'receiver_accepted_spans|receiver_refused_spans|exporter_send_failed'
Accepted rising while the backend stays empty points downstream; refused rising points at the memory limiter and is a capacity problem, covered in handling collector backpressure.
5. Did the process exit before flushing?
import atexit, signal
from opentelemetry import trace
provider = trace.get_tracer_provider()
def _flush(*_):
# Blocks until the queue drains or the timeout expires.
provider.shutdown()
atexit.register(_flush)
signal.signal(signal.SIGTERM, lambda *_: (_flush(), exit(0)))
Verification
Once fixed, prove it end to end with a forced trace rather than by waiting for one to appear:
TID=$(openssl rand -hex 16)
curl -H "traceparent: 00-${TID}-$(openssl rand -hex 8)-01" \
https://api.example.com/checkout/123
sleep 10
curl -s -o /dev/null -w '%{http_code}\n' "http://tempo:3200/api/traces/${TID}"
# 200 — the whole path works, end to end
Keep that as a synthetic check on a schedule. It converts the entire class of problem from “someone notices weeks later” into an alert.
The failures that only appear under load
Three export problems are invisible in a quiet environment and reliable in a busy one, which is why “it works in staging” is such a common part of this investigation.
Queue overflow. The batch processor’s queue is sized in spans, and at low traffic it never fills. At production rates it turns over in a fraction of a second, so any exporter hiccup — a brief network blip, a Collector restart, a slow backend — overflows it and drops whatever arrives next. The symptom is intermittent partial traces rather than complete absence, and the diagnostic is the SDK’s dropped-span counter rather than any error message.
Export timeouts under concurrency. An exporter with a default timeout and a single consumer serialises every batch. If the backend’s latency rises, batches queue behind each other and the effective export rate collapses well before the backend is actually saturated. Raising the consumer count is usually a bigger win than raising the queue size, and the two are frequently confused.
Connection churn. Some deployments recreate the exporter connection per batch — a misconfigured client, an aggressive idle timeout on a load balancer between the SDK and the Collector, or a service mesh terminating idle connections. Each batch then pays TCP and TLS setup, which at high batch rates is a substantial and entirely invisible cost. The tell is export latency that is high and constant regardless of payload size.
All three share a diagnostic property: they show up in counters and not in logs. Exporting the SDK’s own metrics — queue size, dropped spans, export duration — is the single highest-value addition you can make to a pipeline that is intermittently losing data, and it takes one processor and a scrape endpoint.
When the answer is “it is working correctly”
A meaningful share of these investigations end with the pipeline being fine and the expectation being wrong. Three cases account for most of them.
The trace was sampled out, which at a 1% rate is the overwhelmingly likely outcome for any single request. The trace exists but under a different service name, because service.name was unset and the backend filed it under unknown_service. Or the search window excluded it, because a clock offset put the spans minutes away from where the query looked.
All three are worth checking before escalating, and all three are ruled out by the forced-trace test: a request with an explicitly sampled traceparent, fetched by ID rather than searched. If that works, the pipeline is healthy and the question becomes which of the three expectations was wrong — a much shorter conversation than a capacity investigation.
Keep the reproduction, not the fix
Whatever the cause turns out to be, the artefact worth keeping is the forced-trace reproduction rather than the configuration change. A one-line script that sends a request with a known, sampled traceparent and then fetches that ID from the backend is the fastest possible answer to “is the pipeline healthy right now”, and it works identically whether the last failure was a wrong port, a NetworkPolicy, or a missing shutdown flush.
Run it from inside the cluster on a schedule and alert when it fails. That single check covers every stage on this page — creation, sampling, export, transport, acceptance, storage — because a trace that completes the round trip proves all six worked. It is the cheapest observability-of-observability you can build, and it turns the entire class of problem from an investigation into a notification.
Common pitfalls
- Debugging the Collector first. Half of these investigations end at the console exporter, which takes a minute and needs no infrastructure access.
- Testing connectivity from the wrong place. A curl from your laptop proves nothing about a pod behind a NetworkPolicy; run it from inside the workload.
- Ignoring the SDK’s own error output. The exporter logs the reason, at debug level, and the message usually names the exact problem.
- Assuming a restart fixed it. Restarting clears the symptom while the queue refills; verify with a forced trace instead.
- Forgetting sampling. A service configured at 1% produces one trace in a hundred, and the first few requests you test may legitimately produce nothing.
Related
- Diagnosing missing and broken traces — the wider classification this checklist sits inside
- Fixing broken parent-child links across services — when spans arrive but the trace is fragmented
- Handling collector backpressure and queue overflow — when the Collector is the stage that drops them
↑ Back to Diagnosing Missing and Broken Traces