ObservabilityLXXI · Tempo at ScaleTempoScale
Tempo Compactor Scaling
What you'll learn
- Explain why the compactor runs as a singleton by default and what changes when sharding is enabled
- Configure compaction_window, block_retention, and max_compaction_objects for the bucket size the platform produces
- Predict the cost of a stopped compactor over hours, days, and weeks
- Diagnose compactor failure shapes (bucket credentials, ring churn, block-format drift after an upgrade)
- Choose the right cadence: hourly vs daily compaction windows for the trace volume
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
At the end of the month the storage bill was 4.2x the previous month’s. The team had not changed retention or ingest. The compactor pod had been silently failing for ten days after a Helm upgrade moved the bucket credentials into a Kubernetes Secret that the compactor’s service account could not read. Blocks accumulated without merging. The bucket grew past ten million objects. The bill arrived first; query latency rose three days later.
This lesson is the discipline of running the Tempo compactor: why it is a singleton, what its cadence costs, and the failure shapes that show up when it stops.
What it is
The Tempo compactor walks the bucket, merges small blocks into larger ones, and (in v2) deduplicates blocks that share trace IDs. It is mostly stateless but operates on the entire bucket, which is why the default deployment is a singleton.
The compactor is the silent keeper of two operational metrics:
- Block count. The number of objects in the bucket. The dominant driver of query latency.
- Bucket size. The bytes in the bucket. The dominant driver of the storage bill.
When the compactor is healthy, both metrics grow slowly. When it is failing or stopped, both grow faster than ingest.
Why a sysadmin cares
Three operational pains are specific to the compactor:
- Bucket growth. A stopped compactor produces a bucket that grows monotonically. After a week the storage bill reflects weeks of data, not days.
- Query latency. The querier scans blocks per query. A
bucket with millions of objects spends most of its query
time in
ListObjectsV2calls, not in fetching block contents. Query p99 latency rises with block count. - Block-format drift. A Tempo upgrade that changes the block format requires a compactor that understands the new format. A pinned-old chart on a new binary produces blocks the compactor cannot merge.
The compactor is mandatory in microservices mode. The binary tolerates its absence; the bucket does not.
How it works
The compactor runs a per-tenant compaction loop. For each tenant, it:
- Lists the tenant’s blocks in the bucket.
- Groups blocks by their compaction window (default
1h). - Pairs the small blocks within each window.
- Reads both blocks, merges them into a new block, writes the new block back to the bucket.
- Marks the source blocks for deletion.
- Records the deletion in the markers file so the bucket lifecycle policy can reap the bytes.
S3 / GCS / Azure
|
| list blocks per tenant
v
+--------------------------------------+
| Compactor (singleton by default) |
| |
| for each tenant: |
| for each compaction_window: |
| pair small blocks |
| read both |
| merge into new block |
| write new block |
| mark sources for deletion |
| record marker |
+--------------------------------------+
|
v
Bucket lifecycle policy reaps deleted bytes
In v2 the compactor also deduplicates blocks that share trace IDs. A trace that arrived on two ingesters (because the ring rebalanced mid-trace) produces two partial blocks; the compactor merges them into a single complete block.
How to configure it
A production compactor config pins the cadence and the retention. The values below match the production defaults for a mid-scale cluster:
compactor:
compaction:
block_retention: 48h
compaction_window: 1h
max_compaction_objects: 1000000
max_block_bytes: 107374182400 # 100 GiB per merged block
flush_interval: 15m
compacted_block_timeout: 30m
ring:
kvstore:
store: memberlist
storage:
trace:
backend: s3
s3:
bucket_name: tempo-traces-prod
region: eu-west-1
access_key: ${AWS_ACCESS_KEY_ID}
secret_key: ${AWS_SECRET_ACCESS_KEY}
Three details to call out:
block_retention: 48hdeletes blocks older than two days. The compactor marks them; the bucket lifecycle policy reaps the bytes. Both must be configured.compaction_window: 1hmeans the compactor merges all blocks inside each one-hour window into one block. A larger window produces larger merged blocks; a smaller window produces more blocks per cycle. The default is a balance.max_compaction_objects: 1000000caps the number of objects the compactor will read in one cycle. The cap prevents a runaway cycle from saturating the compactor pod.
When the bucket is sharded, the additional values:
compactor:
ring:
kvstore:
store: memberlist
sharding_ring:
enabled: true
kvstore:
store: memberlist
Severity: CONFIGURATION. Reloading the compactor config requires a process restart.
How to validate it
Severity: READ-ONLY.
- Confirm the compactor is ready and registered:
curl -s http://tempo-compactor:3200/ready
# ready
- Confirm compaction is making progress:
curl -s http://tempo-compactor:3200/metrics \
| grep -E '^tempo_compactor_(blocks_compacted|blocks_marked)_total'
# tempo_compactor_blocks_compacted_total 4231
# tempo_compactor_blocks_marked_total 4198
The two counters should track each other within a small
multiple. A flat blocks_compacted counter for hours means the
compactor is wedged.
- Confirm the bucket has a sane block count:
aws s3api list-objects-v2 \
--bucket tempo-traces-prod \
--prefix 'blocks/' \
--max-items 0 \
--query 'Length' \
| jq .
# 482301
A block count growing faster than ingest rate per hour means the compactor is not keeping up.
- Confirm the compactor can reach the bucket:
curl -s http://tempo-compactor:3200/metrics \
| grep tempo_compactor_storage
# tempo_compactor_storage_total_bytes 2.3e11
A missing or zero counter means the bucket is unreachable.
How it can fail
Six shapes appear repeatedly:
- Bucket credentials expired or rotated. The compactor
cannot list blocks. Symptom is the compactor logs
NoCredentialProvidersorAccessDeniedand theblocks_compacted_totalcounter is flat. - Compactor missing after a Helm upgrade. The new chart
does not deploy a compactor by default in some configurations.
Symptom is
kubectl get statefulsetshowing no compactor pod and the bucket growing. - Compaction window too small for the workload. A 10-minute
window with 50k spans/sec produces too many small blocks for
the compactor to merge in one cycle. Symptom is the compactor
using 100% CPU but
blocks_compacted_totaladvancing slowly. max_compaction_objectstoo low. The compactor pauses each cycle after reading the cap, then resumes on the next cycle. Symptom is the cycle metric spiking and theblocks_compacted_totaladvancing in step-wise jumps.- Block-format drift after a binary upgrade. A new Tempo
binary writes blocks in a new format. An old compactor
cannot read them. Symptom is the compactor logs
unknown block formatand skips blocks. A pin mismatch. - Compactor pod OOM-killed on a backlog. A compactor that
has been offline for a week tries to read millions of blocks
in the first cycle. Symptom is
kubectl describe podshowingOOMKilledand the compactor pod restarting repeatedly.
How to troubleshoot it
The diagnostic order:
- Is the compactor pod running?
kubectl get pod -l app=tempo-compactor. A missing pod is a deployment bug. - Can the compactor reach the bucket? Check the compactor logs for credential errors and the bucket policy for the compactor’s IAM role.
- Is the compactor making progress? Check
tempo_compactor_blocks_compacted_total. A flat counter for more than one cycle means something is wedged. - Is the compactor OOM-killed? Check
kubectl describe podforOOMKilledevents. - Is the bucket growing faster than the compactor can drain?
List the bucket and divide by the rate of
blocks_marked_total. - Are blocks the right format? Check the compactor logs for
unknown block formatlines. A pin mismatch between binary versions.
Security implications
The compactor has two attack surfaces:
- Bucket credentials. The compactor reads every block in the
bucket. A leaked AWS key with
s3:GetObjectands3:ListBucketon the Tempo bucket is a trace-data breach. Use scoped credentials and rotate them. - Compactor HTTP endpoint. The
/readyand/metricsendpoints expose operational state. Bind the HTTP listen address to a private interface; do not expose the compactor port externally.
Performance implications
The compactor is CPU- and network-bound during the compaction
cycle. The cost is bounded by the cycle length and the
max_compaction_objects cap. Three knobs:
- Cycle length. Default 15 minutes. A shorter cycle produces smaller merges and finishes sooner; a longer cycle produces larger merges and finishes later.
- Compaction window. Default 1 hour. A larger window merges more blocks per cycle and produces fewer merged blocks in the bucket. The trade-off is per-block read cost.
- Max compaction objects. Default 1,000,000. The cap prevents a single cycle from saturating the compactor pod.
For most platforms a single compactor pod with 4 vCPU and 8 GiB is sufficient for a bucket of up to ~10 million blocks per cycle.
Production guidance
- Run exactly one compactor by default. Only enable sharded compaction when a single compactor cannot keep up with the bucket.
- Set
block_retentionto the longest time you will ever need to query, and configure the bucket lifecycle to match. - Alert on
tempo_compactor_blocks_compacted_totaladvancing and on the bucket object count. - Pin the compactor binary to the same version as the rest of the cluster. A version mismatch produces block-format errors.
Verification
You should now be able to answer:
- Why does the compactor run as a singleton by default?
- What does
compaction_windowcontrol, and what is the trade-off of changing it? - What is the cost of a stopped compactor over hours, days, and weeks?
- How do you validate that the compactor is making progress?
- When is sharded compaction worth the operational cost?
Quiz
Knowledge check · 8 questions
Q1. Why does the Tempo compactor run as a singleton by default?
Q2. What does the compaction_window setting control?
Q3. A stopped compactor prevents new trace ingestion.
Q4. Which of the following are observable effects of a stopped compactor? (select all that apply)
Q5. Name the compactor metric that shows how many blocks have been merged into a new block.
Q6. What does block_retention control?
Q7. Multiple compactors are safe to run without explicit sharding configuration.
Q8. A compactor pod that has been offline for a week is restarted. What is the most likely failure shape?
Passing score: 75%. Answers are checked in this browser.