ObservabilityXLV · Tempo ArchitectureTempoArchitecture
The Compactor
What you'll learn
- Explain the compactor's role in merging small blocks and enforcing retention
- Configure compaction windows, block retention, and the local work directory for production
- Diagnose compactor failure modes (racing compactors, bucket throttling, oversized windows)
- Validate compactor health using tempo_compactor metrics and the marked-for-deletion counters
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 querier takes forty seconds to return a TraceQL result that should take one. The on-call engineer opens the bucket metrics and sees three hundred thousand small blocks. The compactor stopped running three weeks ago, the ingester kept flushing, and the querier now scans every block in the bucket to answer every query. A team restores the compactor; the queue clears in two hours.
This lesson describes the Tempo compactor: why it exists, how it walks the bucket, and what it costs to run.
What it is
The Tempo compactor is a stateless singleton service. Its job is to walk the trace bucket, merge small blocks into larger ones, and delete blocks older than the configured retention. It is the LSM-tree “compact” stage of Tempo’s storage path.
In default configuration, the compactor runs alone. Two compactors racing on the same block can corrupt it, so the service uses a KV-lease to elect a single leader. Sharded compaction is supported but requires explicit configuration.
Why a sysadmin cares
Three operational pains are specific to the compactor:
- Unbounded block growth. The ingester flushes every
max_block_durationper tenant. Without the compactor, the block count grows linearly with time. The querier scans every block that intersects a query window; the cost grows with the block count, not the data size. - Retention enforcement. The compactor is the component that deletes blocks. Tempo deletes by removing the index entry from the block metadata; the bytes are left to the bucket’s lifecycle policy. A misconfigured compactor either keeps too much or deletes too little.
- Trace ID dedup in v2 blocks. Tempo v2 blocks rewrite the data so that traces with shared IDs across multiple blocks are deduplicated. The compactor performs the rewrite; the querier reads the deduped form.
How it works
The compactor runs two background loops on the bucket:
Bucket
|
+-- loop A: scan blocks by tenant + window
| |
| v
| list blocks older than the compaction window
| |
| v
| group by (tenant, window)
| |
| v
| merge into one output block
| |
| v
| write to bucket, mark originals for deletion
|
+-- loop B: scan blocks by tenant + window
|
v
list blocks older than retention
|
v
delete block metadata (bucket lifecycle deletes bytes)
Two loops because the operations have different costs and cadences:
- Compaction loop. Runs continuously. Walks the bucket,
finds small blocks within the compaction window (default 1
hour), and merges them into a single block per tenant per
window. A block that is already larger than
compaction.compaction_block_size_bytesis left alone. - Retention loop. Runs every
compactor.compaction.retention_check_duration(default 1 h). Walks the bucket, finds blocks older thanblock_retention, and marks them for deletion. The bucket lifecycle policy deletes the bytes after a configurable grace period.
How to configure it
A production compactor config pins the compaction window, the retention, and the work directory:
compactor:
compaction:
# Compaction windows are 1 hour. Smaller windows mean more
# frequent compaction; larger windows mean more blocks per
# compactor pass.
compaction_window: 1h
# Max size of a single compacted block. Blocks larger than
# this are not merged with their neighbours.
compaction_block_size_bytes: 536870912 # 512 MiB
# Retention window. Blocks older than this are marked for
# deletion. Pair this with the S3 bucket lifecycle policy.
block_retention: 168h # 7 days
# How often the retention loop runs.
retention_check_duration: 1h
# Local work directory. Must be sized to hold the largest
# compacted block plus overhead. NVMe SSD is the right answer.
working_directory: /var/tempo/compactor
# How many blocks to compact in parallel per tenant.
max_compaction_objects: 100
Three production details to call out:
compaction_window: 1his the right default. Smaller windows produce smaller compactions more often; larger windows produce larger compactions less often and increase the local disk needed.block_retentionand the bucket lifecycle policy must agree. Ifblock_retention: 168hand the lifecycle expires objects after 24 hours, the compactor marks blocks for deletion but the bytes vanish before a recovery could restore them.working_directorymust be on local disk with enough free space for the largest merged block. A 512 MiB compacted block needs at least 1 GiB of free space during the merge.
How to validate it
Five checks confirm the compactor is doing its job:
- Confirm the compactor is ready and elected leader:
curl -s http://tempo.internal:3200/compactor/ready
# ready
curl -s http://tempo.internal:3200/compactor/ring | jq .
# {
# "name": "compactor",
# "members": [{"addr": "tempo-compactor-0:3200", "state": "ACTIVE"}]
# }
- Confirm compaction is progressing. The
tempo_compactor_blocks_compacted_totalcounter should rise over time:
curl -s http://tempo.internal:3200/metrics \
| grep tempo_compactor_blocks_compacted_total
# tempo_compactor_blocks_compacted_total 1284
- Confirm the block count is bounded. Use
aws s3apito count blocks in the bucket:
aws s3api list-objects-v2 \
--bucket tempo-traces-prod \
--prefix 'blocks/ingester/' \
--output json | jq '.KeyCount'
# 4721
A healthy cluster holds tens of thousands of blocks; a broken compactor produces hundreds of thousands.
- Confirm the retention loop is deleting old blocks. The
tempo_compactor_blocks_marked_for_deletion_totalcounter should rise as the retention window advances:
curl -s http://tempo.internal:3200/metrics \
| grep tempo_compactor_blocks_marked_for_deletion_total
# tempo_compactor_blocks_marked_for_deletion_total 384
- Confirm a query that previously scanned many blocks now scans
few. The
tempo_querier_blocks_scanned_per_queryhistogram should trend down after the compactor catches up:
curl -s http://tempo.internal:3200/metrics \
| grep 'tempo_querier_blocks_scanned_per_query_bucket' | head
# tempo_querier_blocks_scanned_per_query_bucket{le="1"} 412
# tempo_querier_blocks_scanned_per_query_bucket{le="10"} 891
How it can fail
Five shapes appear repeatedly:
- Compactor is not running. Block count grows without
bound. Querier latency climbs as the bucket grows. Symptom
is
tempo_compactor_blocks_compacted_totalflat at zero and bucket object count rising without limit. - Two compactors racing. A second compactor pod starts
without realising the first is the leader. Both compact the
same block; one overwrites the other’s output. Symptom is
corrupted blocks appearing in the bucket; the querier logs
parquet: invalid footererrors. - Bucket throttling. The compactor downloads a hundred
blocks at once. S3 throttles with
SlowDown. Symptom istempo_compactor_failed_compactions_totalrising alongside503errors in the compactor logs. - Local disk full. The working directory cannot hold the
next merged block. Symptom is
tempo_compactor_disk_out_of_space_totalrising; compactions fail. - Retention loop not deleting. The
block_retentionwindow is too long, or the bucket lifecycle is misconfigured. Symptom is the bucket size growing past the planned capacity; cost increases silently.
How to troubleshoot it
The diagnostic order:
- Is the compactor running? Check pod status. A crashed compactor is the obvious case; a running compactor that has not registered in the ring is the less obvious one.
- Is the compactor the leader? Check
tempo_compactor_ring_members. More than oneACTIVEmember means a race; pick one and stop the others. - Are compactions succeeding? Check
tempo_compactor_blocks_compacted_total. A flat counter despite a running compactor means the compaction loop is failing. - Is the bucket throttling? Check the compactor logs for
SlowDownfrom S3. If present, raise the throttling retry budget or back off the parallel compaction count. - Is the working directory sized correctly? Check
df -honworking_directory. A full disk means compactions cannot write output blocks. - Is the retention loop deleting blocks? Check
tempo_compactor_blocks_marked_for_deletion_total. A flat counter despite old blocks in the bucket means the retention loop is not running orblock_retentionis wrong.
Security implications
The compactor is local-network-only:
- Bucket credentials. The compactor must write merged blocks to the bucket. The same scoped credentials used by the ingester apply.
- Local work directory. The merged blocks live briefly on local disk. Anyone with read access to the working directory can read span payloads. Restrict the path to root or to a dedicated service user.
- No inbound network. The compactor accepts only the
/readyand/metricsendpoints on its local listen port. No inbound data traffic.
Performance implications
The compactor is CPU- and network-bound during the compaction window:
- CPU. Block decoding, merging, and encoding consume CPU proportional to block size. A 1 GiB compaction consumes roughly one CPU minute on a modern x86.
- Network. The compactor downloads input blocks and uploads output blocks. At 100 MiB/s sustained, the compactor saturates the local link.
- Local disk. The working directory holds the largest merged block. A 512 MiB compacted block needs at least 1 GiB of free space.
- Cost. The compactor performs one compaction per tenant per window per cycle. A cluster with 200 tenants and a 1-hour window performs 200 compactions per hour.
Production guidance
- Run exactly one compactor pod. More than one is a race; less than one is a single point of failure for retention.
- Size the working directory to twice the largest merged block. NVMe SSD is the right answer.
- 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_totalrate. A falling rate means the compactor cannot keep up.
Verification
You should now be able to answer:
- What does the compactor do, and why must it run as a singleton?
- What is the role of
compaction_windowvsblock_retention? - How does the compactor delete a block?
- Why must the working directory be on local SSD rather than network storage?
- What is the operational cost of the compactor not running?
Quiz
Knowledge check · 8 questions
Q1. What is the primary job of the Tempo compactor?
Q2. Why does the compactor run as a singleton by default?
Q3. When the compactor marks a block for deletion, the bytes are removed from the bucket immediately.
Q4. Which of the following are valid compactor flush triggers? (select all that apply)
Q5. A querier that previously returned a query in 200 ms now returns it in 40 s. The most likely cause is:
Q6. Name the compactor metric that confirms compaction work is happening.
Q7. What is the operational effect of running two compactor pods simultaneously?
Q8. The compactor enforces retention by removing block bytes directly.
Passing score: 75%. Answers are checked in this browser.