ObservabilityLXVIII · Prometheus HAPrometheusHA
Thanos and the Query Layer
What you'll learn
- Describe the role of Thanos Sidecar, Store, and Querier in a global Prometheus HA deployment
- Configure --query.replica-label on Thanos Querier to dedup duplicate-scrape pairs
- Recognise the ha_pair label convention used by Cortex and Mimir for the same dedup
- Diagnose wrong dedup behaviour at the query layer using concrete PromQL probes
- Choose between Thanos Sidecar and Thanos Receive for the write path based on the production target
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
Two Prometheuses scrape the same targets. Both ship their
TSDB blocks to an S3 bucket via Thanos Sidecar. The Thanos
Querier fans out queries across both Sidecar stores, and a
panel that should show one CPU value per host shows two,
stacked, with no clear indication that one is from replica a
and the other is from replica b. The dashboards are confusing
the on-call.
Thanos is the query layer that solves this. The Sidecar uploads blocks to object storage; the Querier fetches blocks from multiple stores; the Querier deduplicates series across duplicate-scrape pairs by external label. With the right configuration, the consumer sees one series per source, and the doubling disappears.
What it is
Thanos is a set of components that turn a set of Prometheus servers into a globally queryable, long-term-retention metrics platform. The components relevant to HA are:
- Sidecar. Runs alongside each Prometheus. Reads the Prometheus TSDB on disk, uploads new blocks to object storage (S3, GCS, Azure Blob, MinIO), and exposes the local TSDB as a gRPC store endpoint.
- Store. Reads blocks from object storage, indexes them in memory, and exposes them as a gRPC store endpoint.
- Querier. Fans out queries to one or more store endpoints (Sidecar and Store), applies replica-label deduplication, and returns the result to the consumer (Grafana, API client).
- Compactor. Reads blocks from object storage, deduplicates them at the block level (one block per replica per time range), and produces compacted blocks for long-term storage.
- Ruler. Evaluates recording and alerting rules against the same store endpoints, with the same replica-label deduplication.
For HA, the relevant components are Sidecar, Store, and Querier. The Sidecar is paired one-to-one with each Prometheus; the Store is shared across all Sidecars; the Querier is the consumer-facing component.
The contrast is Thanos Receive. Receive is the alternative
write path: Prometheuses remote_write to a Receive gateway
instead of writing locally and shipping blocks via Sidecar.
Receive is appropriate for Prometheus servers that should not
hold state locally (for example, in a Kubernetes deployment
where pods are short-lived). For HA, the Sidecar pattern is
simpler and is the focus of this lesson.
Why a sysadmin cares
Without a query-layer dedup, the duplicate-scrape pattern described in lesson 01 produces roughly double the metric values on every dashboard. The on-call cannot tell whether the doubling is a real signal (a service is exporting twice the expected traffic) or a configuration error (two Prometheuses are scraping the same target). The investigation has to start from “is the dashboard correct?” before “is the service correct?”
Thanos Querier with the right dedup flag eliminates the doubling. The consumer sees one series per source, regardless of how many Prometheuses scraped the target. The cost is one extra component to operate and a small amount of latency for the dedup step.
How it works
+-------------------+ +-------------------+
| Prometheus A | | Prometheus B |
| replica=a | | replica=b |
| + Thanos Sidecar | | + Thanos Sidecar |
+---------+---------+ +---------+---------+
| |
upload blocks upload blocks
| |
v v
+------------------------------------------------+
| Object Storage |
| blocks from A: fingerprint ...replica=a |
| blocks from B: fingerprint ...replica=b |
+-----------------------+------------------------+
|
v
+----------------------+
| Thanos Store |
| (reads object store) |
+----------+-----------+
|
v
+----------------------+
| Thanos Querier |
| --query.replica-label|
| =replica |
| dedup: keep latest |
| sample per series |
+----------+-----------+
|
v
Grafana / API
one series per
logical source
The Sidecar is colocated with the Prometheus. It watches the Prometheus’s data directory and uploads new blocks to object storage as Prometheus writes them. The Sidecar also exposes the local TSDB as a gRPC store endpoint so the Querier can read recent data without going to object storage.
The Querier is the consumer-facing component. It accepts PromQL queries from Grafana or API clients, fans out the query to every configured store endpoint (Sidecar A, Sidecar B, Store), and applies deduplication. The deduplication groups series by every label except the listed replica label, then within each group keeps the sample with the highest timestamp.
For a duplicate-scrape pair, both Sidecars return their copy of the series. The Querier keeps the one with the higher timestamp and discards the other. The consumer sees one series.
Under the hood
How to configure it
The Sidecar runs alongside Prometheus. The minimal Sidecar configuration:
# /etc/thanos/sidecar.yml
type: S3
config:
bucket: thanos-prod-eu
endpoint: s3.eu-west-1.amazonaws.com
access_key: <aws-access-key>
secret_key: <aws-secret-key>
Start the Sidecar:
# SEVERITY: SERVICE-IMPACT
thanos sidecar \
--tsdb.path=/var/lib/prometheus \
--prometheus.url=http://localhost:9090 \
--objstore.config-file=/etc/thanos/sidecar.yml \
--grpc-address=0.0.0.0:10901 \
--http-address=0.0.0.0:10902
The Sidecar reads the Prometheus TSDB on disk and exposes it as a gRPC store on port 10901.
The Querier configuration is simpler. It only needs the list of store endpoints:
# /etc/thanos/querier.yml (empty file; configuration is via flags)
# SEVERITY: SERVICE-IMPACT
thanos query \
--http-address=0.0.0.0:10902 \
--grpc-address=0.0.0.0:10901 \
--store=sidecar-a.prod.example.com:10901 \
--store=sidecar-b.prod.example.com:10901 \
--store=store-gateway.prod.example.com:10901 \
--query.replica-label=replica
The --store flags list the gRPC endpoints of the Sidecars
and the Store gateway. The --query.replica-label flag tells
the Querier to dedup on the replica label.
For a Cortex deployment, the equivalent dedup is on the ingester:
# cortex ingester config
ingester:
lifecycler:
ring:
kvstore:
store: consul
replicate_factor: 3
ha_tracker:
ha_tracker_timeout: 30s
accept_ha_samples: true
expected_ha_replicas: 2
The accept_ha_samples: true flag tells the ingester to
accept samples from both replicas of an HA pair and dedup
them in memory. The expected_ha_replicas: 2 flag declares
the expected number of replicas; samples beyond that count
are rejected.
How to validate it
The first check is that every Sidecar is reachable from the Querier:
# SEVERITY: READ-ONLY
curl -s http://querier:10902/api/v1/stores | jq
Expected output (illustrative):
[
{ "name": "sidecar-a", "address": "sidecar-a.prod.example.com:10901", "lastCheck": "..." },
{ "name": "sidecar-b", "address": "sidecar-b.prod.example.com:10901", "lastCheck": "..." },
{ "name": "store-01", "address": "store-01.prod.example.com:10901", "lastCheck": "..." }
]
Three stores registered. The Querier will fan out queries to all three.
The second check is that the dedup flag is honoured. Pick a metric that both Prometheuses scrape:
# SEVERITY: READ-ONLY
curl -s --data-urlencode 'query=count by (instance) (up{job="node"})' \
http://querier:10902/api/v1/query | jq '.data.result'
Expected output (illustrative):
[
{"metric": {"instance": "10.0.1.10:9100"}, "value": [1700000000, "1"]},
{"metric": {"instance": "10.0.1.11:9100"}, "value": [1700000000, "1"]}
]
Two rows, one per target. If the result is four rows (two per
target, with replica=a and replica=b labels), the dedup is
not active.
The third check is that the Sidecar is uploading blocks. Query the Sidecar’s metrics:
# SEVERITY: READ-ONLY
curl -s http://sidecar-a:10902/metrics | grep thanos_shipper
Expected output (illustrative):
thanos_shipper_uploads_total 432
thanos_shipper_upload_errors_total 0
A non-zero upload count and zero errors. The error counter rising means the Sidecar cannot write to object storage.
The fourth check is that the Compactor is keeping up. The Compactor produces compacted blocks; without it, the object store grows indefinitely with overlapping blocks per replica.
# SEVERITY: READ-ONLY
curl -s http://compactor:10902/metrics | grep thanos_compact
Expected output (illustrative):
thanos_compact_blocks_cleaned_total 14
thanos_compact_group_merges_total 7
A non-zero compaction count. If the compactor has been running for hours and the counters are still zero, the Compactor is not progressing.
How it can fail
Six failure modes appear repeatedly in production Thanos deployments.
- Dedup flag points to the wrong label. The Querier is
configured with
--query.replica-label=prometheusinstead of--query.replica-label=replica. Symptom: every series is dedupped to one Prometheus, not one per logical source. The fix is to set the flag to the label that varies per duplicate-scrape replica of the same Prometheus (typicallyreplica). - Sidecar upload fails with hash conflict. Two
Prometheuses share the same
prometheusexternal label. Symptom:thanos_shipper_upload_errors_totalrises witherror="hash conflict". The fix is to set a uniqueprometheusexternal label per instance. - Querier cannot reach a Sidecar. The Sidecar host is
unreachable (DNS, firewall, network policy). Symptom: the
affected Prometheus’s data is missing from the unified
view. The fix is to restore connectivity and to monitor the
thanos_query_store_apis_foundmetric. - Compactor falls behind. The Compactor cannot keep up with the block upload rate. Symptom: the object store accumulates overlapping blocks; the indexer slows down; the Querier returns results more slowly. The fix is to scale the Compactor (more CPU, more memory, more parallelism).
- The replica label is not set on the Prometheus. The
Sidecar uploads blocks; the series carry no
replicalabel; the Querier cannot dedup. Symptom: the doubled-series problem from lesson 01 returns. The fix is to add thereplicaexternal label to the Prometheus and to restart. - Cortex ingester is configured for HA but the
accept_ha_samplesflag is false. Symptom: the second replica’s samples are rejected with anot accepting HA sampleserror; one Prometheus’s data is missing. The fix is to enableaccept_ha_samples: trueand to declare theexpected_ha_replicas.
How to troubleshoot it
The diagnostic order when a Thanos-backed dashboard shows duplicated series:
- Confirm the Sidecar stores are registered with the
Querier.
curl /api/v1/storeson the Querier. - Confirm the dedup flag is set on the Querier. Inspect the process flags.
- Confirm the
replicalabel is present on the series. Query the Querier forcount by (replica) (up). - Confirm the Sidecars are uploading blocks. Inspect the
thanos_shipper_upload_errors_totalmetric. - Confirm the Compactor is running. Inspect the
thanos_compact_blocks_cleaned_totalmetric. - Confirm the network path between every Sidecar and the
Querier.
nc -zv sidecar-a 10901from the Querier host.
Security implications
Thanos components expose gRPC and HTTP endpoints. The gRPC port (10901) is for inter-component traffic; it should be firewalled to allow traffic only from the cluster’s node IPs. The HTTP port (10902) is for the API and metrics; it should require authentication for the API endpoints that allow mutations (store removal, config reload).
Object storage credentials are stored in the Sidecar configuration file. The file should be readable only by the Sidecar user, and the credentials should be sourced from a secret store (Vault, AWS Secrets Manager) rather than committed to the repository.
The block upload identifies the block by the prometheus
external label. A misconfiguration that exposes internal
cluster identifiers in the prometheus label is a small
information disclosure. The convention is a short opaque
identifier (prod-eu-a, prod-eu-b).
Performance implications
The Sidecar adds CPU and memory overhead to the Prometheus host. The Sidecar reads the local TSDB on every block upload (typically every two hours); the CPU cost is small. The memory cost is dominated by the index, which scales with the number of series. For a 10 million series Prometheus, the Sidecar may use 4-8 GiB of memory.
The Querier is memory-bound. The Querier holds the result set
from every store in memory during the dedup step. For queries
that return many series (a count by (job) (up) against a
fleet), the result set can be large. The Querier’s default
query timeout is 2 minutes; long-running queries should be
sized accordingly.
The Compactor is CPU-bound. The Compactor reads every block from object storage, deduplicates the data per replica, and writes compacted blocks back. The work scales with the total data volume. A Compactor that falls behind produces an ever-growing set of overlapping blocks in object storage, which the indexer must process on every query.
Production guidance
- Use the
replicaexternal label convention. The default--query.replica-label=replicamatches the convention; a non-default label name requires every operator to know the convention. - Run at least one Store gateway per availability zone. The Store reads object storage on every query; the latency penalty for a single Store is too high for a production dashboard.
- Run a single Compactor per cluster. Multiple Compactors
race on the same blocks and produce duplicate work. The
Compactor uses a
--compactor.concurrencyflag to parallelise within a single process. - Monitor
thanos_query_store_apis_foundand alert when the count drops below the expected number of stores. - Monitor
thanos_compact_blocks_cleaned_totaland alert when the rate drops to zero for more than a few hours. - For Cortex or Mimir deployments, set
accept_ha_samples: trueon the ingesters and declare the expected number of replicas.
Verification
You should now be able to answer:
- What are the three Thanos components relevant to Prometheus HA, and what is the role of each?
- Which flag tells Thanos Querier to deduplicate series across duplicate-scrape pairs, and what is the default value?
- How does Cortex’s
accept_ha_samplesflag differ from Thanos Querier’s--query.replica-labelflag, and where does the dedup happen in each? - What is the most common symptom of a misconfigured
--query.replica-labelflag, and how do you diagnose it? - What is the role of the Thanos Compactor, and how do you know it is keeping up?
Quiz
Knowledge check · 8 questions
Q1. What is the role of Thanos Sidecar in a HA Prometheus deployment?
Q2. What is the default value of --query.replica-label on Thanos Querier?
Q3. Thanos Querier deduplicates series by keeping the sample with the highest timestamp within each group.
Q4. The Querier is configured with --query.replica-label=prometheus instead of --query.replica-label=replica. What is the consequence?
Q5. Name one Thanos component that performs block-level deduplication over time.
Q6. Which of these are Thanos components in a HA Prometheus deployment?
Q7. A Cortex deployment runs two HA replicas. The second replica samples are rejected with not accepting HA samples. The missing setting is:
Q8. The Compactor has been running for 12 hours and thanos_compact_blocks_cleaned_total is zero. The most likely cause is:
Passing score: 75%. Answers are checked in this browser.