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.

Jaeger, Tempo, and Zipkin on query power versus storage cost Jaeger sits at high query power and high cost. Tempo sits at low cost with moderate query power via TraceQL and correlation. Zipkin sits at low cost and moderate query power at small scale. Storage & index cost at scale → Query power without a trace ID → Zipkin small-scale index Tempo TraceQL + correlation Jaeger full attribute index
Jaeger buys query power with index cost; Tempo trades index breadth for the cheapest storage; Zipkin holds a middle ground that works best at smaller volume.

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 :9411 drops spans. Always route through a Collector with the zipkin exporter, 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.

↑ Back to Trace Storage Backend Comparison: Jaeger vs Tempo