Jaeger vs Tempo vs Zipkin: decision guide
Choose Tempo when trace volume and storage cost dominate and you can pivot from logs or metrics into a trace ID; choose Jaeger when you need rich attribute search out of the box; choose Zipkin when you want a lightweight, battle-tested backend for a small or legacy deployment.
Context and when it matters
Every distributed tracing rollout eventually forces a storage decision, and the wrong one is expensive to unwind after months of retained data. The three most widely deployed open-source backends — Jaeger, Tempo, and Zipkin — solve the same surface problem (persist and retrieve traces) but make opposite bets on how a span is indexed. That single design choice cascades into cost, query power, and operational burden.
The tension is index breadth versus storage economics. Jaeger indexes span attributes so you can search “all traces where http.status_code = 500 on checkout-service” without knowing a trace ID. Tempo deliberately indexes almost nothing, storing raw trace blocks in object storage keyed by trace ID, and expects you to arrive with an ID discovered from a correlated log or metric. Zipkin sits in between with a compact attribute index tuned for modest scale. Picking correctly means being honest about how your engineers actually start a debugging session.
Side-by-side comparison
| Dimension | Jaeger | Grafana Tempo | Zipkin |
|---|---|---|---|
| Storage model & backend deps | Pluggable: Cassandra, Elasticsearch/OpenSearch, or the newer Badger/embedded store; heavier operational surface | Object storage only (S3, GCS, Azure Blob, or local); no database to run | Pluggable: in-memory, MySQL, Cassandra, or Elasticsearch |
| Indexing & search | Full attribute index — search by service, operation, tags, duration without a trace ID | Trace-ID-first; attribute search via TraceQL block scan or a metrics-generator, best paired with logs/metrics correlation | Attribute index over service, span name, and annotations; good at modest cardinality |
| Query language | Jaeger UI filters + JSON API; no rich query DSL | TraceQL — a purpose-built trace query language with span-level predicates | Zipkin API query params (serviceName, spanName, tags, lookback) |
| Cost at scale | Higher — index storage in Cassandra/ES grows with cardinality and often rivals raw span size | Lowest — object storage is cheap and the near-absent index keeps write amplification minimal | Moderate — index footprint acceptable at small/medium volume, grows with tags |
| Ingestion protocol / OTLP | Native OTLP (gRPC + HTTP), plus legacy Jaeger Thrift/gRPC | Native OTLP (gRPC + HTTP), plus Jaeger and Zipkin receivers | Zipkin v2 JSON/Protobuf; OTLP only via a Collector translation hop |
| Operational complexity | Medium-to-high — you own a Cassandra or Elasticsearch cluster and its scaling | Medium — no database, but object-store lifecycle, compaction, and TraceQL tuning to manage | Low — single service, in-memory or one small database for many teams |
| Ecosystem & UI | Mature standalone UI, service dependency graph, wide CNCF adoption | Deep Grafana integration; traces-to-logs and traces-to-metrics links are first-class | Simple, stable UI; large historical install base and many client libraries |
| Best for | Attribute-driven debugging without a known trace ID | High-volume, cost-sensitive fleets with strong logs/metrics correlation | Small teams, legacy Zipkin stacks, low-ceremony deployments |
Positioning: cost versus query power
The trade-off is easiest to see on two axes. The horizontal axis is ingest-and-store cost per span at scale; the vertical axis is how much you can ask of the backend without already holding a trace ID.
Implementation: pointing a Collector at each backend
The cleanest way to stay backend-agnostic is to keep your services emitting OTLP and let the OpenTelemetry Collector fan out to whichever backend you choose. Only the exporter block changes.
receivers:
otlp:
protocols:
grpc: # services push OTLP/gRPC here
http: # OTLP/HTTP for browser + serverless clients
exporters:
# --- Jaeger: OTLP is native, so no translation is needed ---
otlp/jaeger:
endpoint: jaeger-collector:4317
tls:
insecure: true
# --- Tempo: also OTLP-native; distributor listens on 4317 ---
otlp/tempo:
endpoint: tempo-distributor:4317
tls:
insecure: true
# --- Zipkin: needs the zipkin exporter to translate OTLP spans
# into Zipkin v2 JSON before the POST ---
zipkin:
endpoint: http://zipkin:9411/api/v2/spans
format: json
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch] # batch to amortize network round-trips
exporters: [otlp/tempo] # swap this line to migrate backends
Because the receiver and processing stages are identical, migrating between backends is a one-line exporter change plus a dual-write window while you backfill dashboards. Keep head-based versus tail-based sampling decisions in the Collector rather than the backend, so sampling policy survives a storage migration untouched.
Decision rules
Choose Tempo when trace ingest volume is high and object-storage economics matter more than ad hoc attribute search. Tempo shines if your engineers already pivot into traces from a log line or an exemplar on a metrics dashboard — they arrive holding a trace ID, which is exactly Tempo’s fast path. Its Grafana-native traces-to-logs and traces-to-metrics links make it the natural pick for teams standardized on the Grafana stack.
Choose Jaeger when debugging starts without a known trace ID and engineers need to ask attribute questions directly: “show slow payment-service traces in the last hour with error=true.” Jaeger’s native index answers that instantly, and its service dependency graph and standalone UI do not require a wider observability platform. Budget for the Cassandra or Elasticsearch cluster that powers the index.
Choose Zipkin when you want the lowest operational ceremony, are running a small or bounded-scale deployment, or already have services emitting Zipkin v2 spans. It is the least demanding backend to stand up and keep alive, and its long history means client-library support is broad. It is the weakest fit for a greenfield OTLP-native, high-cardinality environment.
Common pitfalls
- Assuming Tempo can replace Jaeger’s search one-to-one. Without a metrics-generator or disciplined TraceQL usage, “find the trace” becomes “scan a lot of blocks.” Adopt Tempo only when your correlation story (logs or exemplars carrying the trace ID) is real, not aspirational.
- Letting Jaeger’s index outgrow your budget silently. High-cardinality attributes inflate the Cassandra/Elasticsearch index until it rivals or exceeds raw span storage. Prune noisy tags before ingest and align retention with the Jaeger retention policy guidance.
- Feeding Zipkin OTLP without a translation hop. Zipkin does not speak OTLP; pointing an OTLP exporter straight at
:9411drops spans. Always route through a Collector with thezipkinexporter, and verify the trust boundary on that hop per security boundaries in distributed tracing.
FAQ
Can I query Tempo by service name or HTTP status like Jaeger?
Yes, but only if you enable TraceQL search over object storage or run a metrics-generator. Vanilla Tempo indexes only the trace ID, so arbitrary attribute search either scans blocks (slower) or relies on a separate label store. Jaeger indexes attributes natively in its backend.
Is Zipkin still a reasonable choice in 2026?
Zipkin remains a solid, low-footprint option for small deployments and legacy stacks already emitting Zipkin v2 spans. For greenfield OpenTelemetry-native systems, Jaeger or Tempo are better aligned with OTLP and long-term ecosystem support.
Do all three accept OTLP directly?
Jaeger and Tempo accept OTLP natively over gRPC and HTTP. Zipkin accepts its own JSON and Protobuf formats; to feed it OTLP you run an OpenTelemetry Collector with a zipkin exporter that translates spans on the way in.
Related
- Configuring Jaeger Retention Policies for Compliance — set TTLs and index retention once you have picked a backend.
- Choosing Between Head-Based and Tail-Based Sampling — control what actually reaches storage regardless of backend.
- OpenTelemetry Collector Pipeline Configuration — the fan-out layer that keeps you backend-agnostic.