ObservabilityLXXI · Tempo at ScaleTempoScale
Tempo Performance Tuning
What you'll learn
- Apply a diagnostic order for slow Tempo queries that rules out each layer in sequence
- Distinguish query-side, ingester-side, and bucket-side bottlenecks from observable symptoms
- Tune the querier concurrency, the query-frontend cache, and the search max-concurrent queriers for the workload
- Recognise the most common cause (bucket block count) and avoid the typical mistake of tuning the querier first
- Set realistic query latency SLOs and the alerts that catch regressions before users notice
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
A trace query took 45 seconds. The on-call engineer spent an
hour tuning the querier pod’s CPU limits and adding another
replica. The query still took 45 seconds. The actual cause was
that the compactor had not run in 12 days and the bucket held
8 million blocks. The querier’s ListObjectsV2 call was
the dominant cost; the CPU was idle waiting for the bucket.
The fix was to restore the compactor, drain the backlog, and
tune the query-frontend cache for the future.
This lesson is the discipline of Tempo performance tuning: the diagnostic order for slow queries, the most common cause, and the knobs that actually move query latency.
What it is
Tempo performance tuning is the discipline of identifying which component is the bottleneck and applying the right knob. Most slow-query cases are not query-side problems; they are bucket- side problems.
Three layers can be the bottleneck:
- Bucket. The querier lists blocks per query. A bucket with
millions of objects spends most of its query time in
ListObjectsV2calls. - Ingester. A head-block-heavy ingester takes longer to answer trace-by-ID queries against recent data. The querier reads the head block first, then falls back to compacted blocks.
- Querier. A querier pool that is undersized for the query load queues requests. The query latency rises because the queue grows, not because any single query is slow.
The diagnostic order matters. Tuning the querier when the bucket is the bottleneck is the most common mistake.
Why a sysadmin cares
Three operational pains are specific to query performance:
- Adoption drops. Engineers stop using Tempo when queries are slow. They revert to logs and metrics, which loses the cross-service correlation that traces provide.
- Investigation time rises. A 45-second query is not a one-off cost; it is the cost of every investigation. Engineers learn to avoid the trace UI; MTTR rises.
- Storage bill grows. A common reaction to slow queries is to add more querier pods. More queriers do not fix a bucket problem; they just spend more CPU waiting on the bucket.
The right approach is to identify the bottleneck first and apply the matching knob.
How it works
A TraceQL query flows through three layers:
Grafana
|
v
+---------------------------+
| query-frontend |
| split, cache, fan-out |
+---------------------------+
|
v
+---------------------------+
| querier |
| TraceQL execution |
| list blocks (bucket) |
| fetch candidate blocks |
| merge results |
+---------------------------+
|
| (for recent data)
v
+---------------------------+
| ingester (head blocks) |
+---------------------------+
|
v
+---------------------------+
| object storage |
+---------------------------+
The query latency budget for a healthy Tempo cluster, end-to- end, is roughly:
- Query-frontend overhead. 1-5 ms for cache hits; 10-50 ms for cache misses.
- Querier list call. 10-100 ms for a bucket with hundreds of thousands of blocks; 1-10 seconds for a bucket with millions of blocks.
- Querier fetch. 50-500 ms per candidate block, depending on block size and network.
- Querier merge. 1-10 ms.
- Head-block read. 10-100 ms for recent data.
The dominant cost in a healthy cluster is the querier list call. When that grows past 100 ms, the bucket is the bottleneck.
How to configure it
A production querier config pins the query concurrency and the search parameters. The query-frontend config pins the cache:
querier:
frontend_worker:
frontend_address: tempo-query-frontend:9095
max_concurrent_queries: 200
search:
max_concurrent_queriers_per_query: 4
query_frontend:
max_concurrent_queries: 200
results_cache:
cache:
embedded_cache:
max_size_items: 1024
ttl: 1h
compactor:
compaction:
block_retention: 48h
compaction_window: 1h
storage:
trace:
backend: s3
s3:
bucket_name: tempo-traces-prod
region: eu-west-1
Three details to call out:
max_concurrent_queriescaps the number of queries a querier pod serves at once. A pod with 4 vCPU and 8 GiB handles roughly 50-100 concurrent queries before the CPU saturates. Beyond that, queries queue.max_concurrent_queriers_per_querycaps the number of parallel subqueries the querier fans out across. A value of4means each query is split into four parallel subqueries. Higher values help when the bucket is the bottleneck; lower values help when the bucket is healthy.results_cache.ttlis the query-frontend cache TTL. A value of1his the production default. A TTL of0disables the cache. A TTL of24hreturns stale results after a config change.
Severity: CONFIGURATION. Reload requires a process restart for most settings.
How to validate it
Severity: READ-ONLY.
- Confirm query latency percentiles:
curl -s http://tempo-querier:3200/metrics \
| grep tempo_querier_query_seconds
# tempo_querier_query_seconds_bucket{le="1"} 12345
# tempo_querier_query_seconds_bucket{le="5"} 14200
# tempo_querier_query_seconds_bucket{le="+Inf"} 14302
A le="1" count close to the le="+Inf" count means most
queries are fast. A le="5" count close to the le="+Inf"
count means most queries take more than 5 seconds.
- Confirm blocks per query:
curl -s http://tempo-querier:3200/metrics \
| grep tempo_querier_search_blocklist_latency_seconds
# tempo_querier_search_blocklist_latency_seconds_bucket{le="0.1"} 11000
# tempo_querier_search_blocklist_latency_seconds_bucket{le="+Inf"} 14200
A le="0.1" count close to the le="+Inf" count means most
list calls are fast. A flat distribution across buckets means
the list call is the bottleneck.
- Confirm the query-frontend cache hit rate:
curl -s http://tempo-query-frontend:3200/metrics \
| grep tempo_query_frontend_cache
# tempo_query_frontend_cache_hits_total 8934
# tempo_query_frontend_cache_misses_total 1245
A hit-to-miss ratio of 7:1 is healthy. A ratio of 1:1 means the cache is not effective.
- Confirm the bucket block count:
aws s3api list-objects-v2 \
--bucket tempo-traces-prod \
--prefix 'blocks/' \
--max-items 0 \
--query 'Length'
# 482301
A block count past 1 million is a query-latency warning.
- Confirm the compactor is keeping up:
curl -s http://tempo-compactor:3200/metrics \
| grep tempo_compactor_blocks_compacted_total
# tempo_compactor_blocks_compacted_total 4231
A flat counter for hours means the compactor is wedged and the bucket is growing.
How it can fail
Six shapes appear repeatedly:
- Compactor wedged. The bucket grows monotonically. The
querier list call latency rises. Symptom is
tempo_querier_search_blocklist_latency_secondsp99 rising alongsidetempo_compactor_blocks_compacted_totalflat. - Querier pool undersized. Query latency rises but ingest
is healthy. The querier CPU is near saturation. Symptom is
tempo_querier_query_secondsp99 rising and the querier CPU pegged. - Query-frontend cache disabled. Every query is a fresh execution. The querier load is higher than necessary. Symptom is the cache miss counter equal to the total request counter.
- Block-format drift after a binary upgrade. The querier
cannot read blocks in the new format and falls back to
scanning the older format. Symptom is the querier logging
unknown block formatand serving slow queries against recent data. - Network latency to the bucket. Cross-region bucket
access adds 50-200 ms to every block fetch. Symptom is
tempo_querier_fetch_duration_secondsp99 rising alongside the bucket’s region. - Query too broad. A TraceQL query without a time range filter scans every block in the bucket. Symptom is a single user query consuming the querier’s concurrency budget for minutes.
How to troubleshoot it
The diagnostic order, cheapest first:
- What is the query p99 latency?
tempo_querier_query_seconds. - What is the list-call latency?
tempo_querier_search_blocklist_latency_seconds. - Is the compactor running?
/readyand the compactor metric. - What is the bucket block count?
aws s3api list-objects-v2. - Is the querier CPU saturated?
kubectl top pod. - Is the query-frontend cache effective? Cache hit/miss ratio.
If the list-call latency is the dominant cost, the bucket is the bottleneck. Fix the compactor. If the querier CPU is saturated, add pods. If the cache hit rate is low, tune the cache TTL or the query patterns.
Security implications
The querier has two attack surfaces:
- Query rate limiting. A single user running a broad
TraceQL query can consume the querier’s concurrency
budget. A
max_concurrent_queriescap is the simplest guard; per-user limits are better. - Trace data exposure. The querier returns trace contents. Authentication is required; unauthenticated access leaks every trace in the tenant.
Performance implications
Query performance has four knobs:
- Compactor. Keeps block count bounded. The dominant fix.
- Querier pool size. Caps concurrency. Add pods for more concurrent queries.
- Query-frontend cache. Reduces querier load for repeated queries. Tune TTL for the query patterns.
- Search parameters.
max_concurrent_queriers_per_querycaps the per-query fan-out. Higher values help when the bucket is the bottleneck.
The right tuning depends on the bottleneck. Measure first; tune second.
Production guidance
- Alert on
tempo_querier_query_secondsp99. A p99 over 5 seconds is a regression. - Alert on
tempo_querier_search_blocklist_latency_secondsp99. A p99 over 200 ms means the bucket is the bottleneck. - Alert on the bucket object count. A count past 1 million is a query-latency warning.
- Run the compactor. The single most common Tempo performance problem is a stopped compactor.
Verification
You should now be able to answer:
- What is the most common cause of slow Tempo queries?
- What is the diagnostic order for a slow query?
- Which knob is the dominant fix for a bucket-side bottleneck?
- When is adding querier pods the right fix?
- Why does the query-frontend cache matter for query performance?
Quiz
Knowledge check · 8 questions
Q1. What is the most common cause of slow Tempo queries?
Q2. Which metric is the right indicator of a query-side (not bucket-side) bottleneck?
Q3. Adding more querier pods always reduces slow-query rate.
Q4. Which of the following are valid levers for improving Tempo query performance? (select all that apply)
Q5. Name the metric that shows how long the querier spends listing candidate blocks per query.
Q6. What is the first diagnostic step for a slow Tempo query?
Q7. The query-frontend cache is enabled by default with a 1-hour TTL.
Q8. A TraceQL query that omits a time-range filter scans which blocks?
Passing score: 75%. Answers are checked in this browser.