ObservabilityLXVII · High AvailabilityHA
Stateful HA
What you'll learn
- Explain why ingester, store-gateway, and compactor replicas cannot be scaled like queriers and what the WAL protects
- Configure replication_factor and the write quorum for Loki, Tempo, and Mimir ingesters and verify the ring is healthy
- Distinguish replication (copies in different zones) from partitioning (shards) and the failure each protects against
- Recognise the failure modes specific to stateful HA: replication_factor drift, WAL replay storms, ring rebalance storms, and divergent compactor views
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 Loki ingester restarts after a routine OOM kill at 04:12.
Its WAL files are intact on the local disk. The replica re-reads
the WAL, reconstructs the in-memory series, and joins the ring
again. The other two ingesters in the ring kept serving writes
during the restart because each write was a triple replicated
across the three owners. By 04:13 the restarted ingester is
serving traffic again. The customer never noticed. This is what
replication buys you. It also buys you a more subtle failure
mode: a misconfigured replication_factor: 1 causes the same
restart to lose the last 90 seconds of writes, with no log line
loud enough to wake the on-call engineer.
Stateful HA is the lesson that prevents the second outcome.
Why “just add replicas” is the wrong answer
The stateful components in the observability stack — Loki ingesters, Mimir ingesters, Tempo ingesters, store-gateways, compactors — own a partition of the data. “Add a replica” is meaningless unless you also tell the system what the new replica should own, how many copies of each piece of data must exist, and how the system should recover when one of the copies dies.
Two mechanisms make this work:
- A hash ring that maps each key (a stream ID in Loki, a tenant in Mimir, a trace ID in Tempo) to a set of owners. Each replica claims a contiguous range of the ring.
- Replication of the write to multiple owners before the
write is acknowledged to the client. The number of owners
is the
replication_factor.
Together they ensure that for every key, R replicas hold the data at any moment. When one replica dies, the system reshuffles the ring and brings a new replica up to R copies.
What the WAL is and what it protects
The write-ahead log is the file that ingester processes append every incoming write to before acknowledging the write. The sequence is:
client
|
v
distributor --hash(key)--> ingesters[R owners]
|
| 1. fsync to local WAL
| 2. update in-memory series
| 3. ack to distributor
v
disk
Three properties matter:
- Durability. Until the WAL is fsynced, the write is not durable. A power loss between the write arriving and the fsync loses the write. The fsync is the price of durability.
- Recovery. After a process restart, the WAL is replayed in order and the in-memory state is reconstructed. Without the WAL, the in-memory state has to be rebuilt from peers, which is slow and depends on every peer being reachable.
- Bounded loss. With WAL fsync on, the loss window is “what was buffered but not yet fsynced when the process died.” That is sub-second on a healthy disk. Without WAL fsync, the loss window is “everything since the last checkpoint.”
Configuring replication
The two settings that matter are replication_factor and the
write quorum. The default for a single-node Loki install is
replication_factor: 1 and the doc comment warns you to change
it for production. Change it.
# loki.yaml — ingester section
ingester:
lifecycler:
ring:
kvstore:
store: consul
replication_factor: 3 # copies per stream
heartbeat_period: 5s
heartbeat_timeout: 1m
# JoinAfter: 0s makes the ingester accept traffic as soon
# as it joins the ring. Set higher in dev so you can curl
# /ready before the ring sees you.
join_after: 0s
observe_period: 0s
final_sleep: 0s
# WAL configuration
wal:
enabled: true # on by default in 3.x
checkpoint_interval: 5m # flush chunk state to disk
flush_on_shutdown: true
The same shape applies to Mimir (ingester.ring.replication_factor)
and Tempo (ingester.ring.replication_factor).
What “3” actually buys
With replication_factor: 3, every write goes to three
ingesters. You can lose any two and still have a complete copy
on the third. The cost is 3x the per-stream memory and 3x the
per-stream WAL I/O. For most teams below ~5 TB/day, the cost is
worth the protection. Above that, capacity planning changes
(see lesson 06).
The write quorum is implied: with R=3 and the default quorum
of quorum: false for Loki distributors, the write succeeds
when at least one replica has fsynced. For a stronger
guarantee, set the distributor’s replication factor to match
and use distributor.ingestion_rate_limit and the
distributor.write_failures counter to detect degradation.
How to validate replication is real
Three checks confirm the ring is configured correctly.
# READ-ONLY
# 1. Confirm the ring has the expected number of healthy owners.
curl -s http://loki-distributor:3100/ring | jq '
.shards[] | {
zone: .zone,
state: .state,
owners: [.owners[] | select(.state == "ACTIVE") | .addr]
}
'
# expected: every shard lists three ACTIVE owners
# READ-ONLY
# 2. Confirm the replication factor is what you set.
curl -s http://loki-distributor:3100/config | \
jq '.loki.config.ingester.lifecycler.ring.replication_factor'
# expected: 3
# READ-ONLY
# 3. Confirm every replica reports the same stream count for
# a known stream after the write completes. Hash drift between
# replicas indicates a misconfigured distributor hash.
for ing in ingester-1 ingester-2 ingester-3; do
curl -s http://$ing.loki.svc:3100/metrics | \
grep loki_ingester_streams
done
# expected: at least two of three replicas show non-zero count
# for any given stream; the third is the replicated copy.
Failure modes specific to stateful HA
The five modes that appear repeatedly:
- Replication factor drift. An ingester is added with
replication_factor: 1left in the config (the default). The system silently treats the new replica as a singleton owner, and writes to its partition are not replicated. The failure surface: a restart of that replica loses its window. - WAL replay storm. A flaky disk produces WAL segments that fail to parse. Every replica that tries to recover from a shared WAL mount gets stuck in the same parse loop. Fix: per-replica WAL directories on local disk, never a shared NFS mount.
- Ring rebalance storm. One zone disappears (a network
partition). The ring redistributes the dead zone’s owners to
the survivors. If the dead zone comes back, the redistribution
happens again. With a WAL replay on top, the cluster spends
more time rebalancing than serving. Fix:
zone-awarereplication with explicit failure-domain labels. - Divergent compactor. Two compactors run because the “one active, one standby” gate failed (KV store split brain). Each compactor rewrites the same index blocks. The result is duplicate work and an index that disagrees with itself. Fix: a real leader-election backend (etcd, Consul) and a fence token.
- Clock skew on WAL timestamps. A replica with a clock
30s in the future writes WAL segments with future timestamps.
The replay window for a later flush then miscalculates. Fix:
run chrony on every node and alert on
node_timex_offset_seconds.
How to troubleshoot a degraded stateful tier
The diagnostic order is:
- Check the ring.
GET /ringon the distributor or querier returns the owner set for every shard. The owner count per shard tells you whether the cluster has lost a replica. - Check the ingester health.
GET /readyper ingester tells you which replicas are willing to accept traffic. - Check the WAL.
du -sh /loki/walper ingester tells you whether WAL is growing (good, normal) or has stalled (bad, flush is failing). The metricloki_ingester_wal_bytes_per_partitionshould be bounded. - Check the flush.
loki_ingester_chunk_age_secondstells you how old the unflushed data is. Above 2h you are reading from WAL, not from chunks. - Check the compactor.
loki_compactor_runningshould be 1. Above 1 means split brain.
Security and performance implications
- Replication factor interacts with tenant isolation. A
tenant whose streams are owned by ingesters that also own
a noisy neighbour’s streams shares fate with that neighbour.
Use
ingester.partition_ringwith explicit per-tenant partitions when isolation matters. - WAL disk I/O is the bottleneck. Replication factor 3 triples WAL I/O. Plan SSD capacity at 2x peak WAL throughput to leave headroom for fsync storms.
- The compactor is a stateful singleton. It cannot be replicated active-active. The standby is hot only after the active dies; the active’s death costs the flush window.
Production guidance
Verification
You should now be able to answer:
- Why does the WAL exist and what does it protect?
- What does
replication_factor: 3actually buy in terms of failure tolerance? - How do you confirm the ring has the expected number of active owners?
- What is the difference between replication and partitioning?
Quiz
Knowledge check · 8 questions
Q1. What does the ingester WAL protect against?
Q2. A Loki ingester runs with replication_factor 3 and a single zone fails. What is the data-loss outcome?
Q3. Which of the following are valid stateful-HA failure modes? (Select all that apply.)
Q4. The ingester WAL may be placed on a shared NFS mount as long as the fsync flag is set.
Q5. The compactor metric loki_compactor_running shows 2. What does that mean?
Q6. Name the three checks that confirm a stateful ring is healthy.
Q7. Replication protects against what kind of failure?
Q8. Ingesters can be replicated active-active like queriers without configuring a ring or replication factor.
Passing score: 75%. Answers are checked in this browser.