ChistaDATA point of view · ClickHouse 26.9.17.8 · October 2026
Most ClickHouse estates we assess are described as highly available, and most of them are, in the narrow sense that a single server can fail without an outage. Fewer can say what happens when a whole availability zone goes, when Keeper sits a region away, or when both copies of a shard disappear at once. Those are the events that decide whether a board hears about an incident. Most designs we see have never been tested against them.
This paper sets out the ClickHouse high availability architecture we recommend for production analytics on the current release, ClickHouse 26.9.17.8. It covers the measurements behind each recommendation, a five-layer framework for assessing an existing estate, and a prioritised roadmap. Every figure comes from our own test cluster: four servers in two data zones, with a three-member ClickHouse Keeper ensemble spread across three zones.
Executive summary
Five findings on ClickHouse high availability architecture
We built a production-shaped ClickHouse cluster and subjected it to the events that cause real outages: zone loss, shard loss, distant coordination and version upgrades. Five findings stand out, and each is developed in the sections that follow.
| # | Finding | Implication for leadership |
|---|---|---|
| 1 | A two-data-zone layout with a Keeper witness in a third zone survived the loss of a full zone. Of 851 requests during the outage, none failed, and the zone was back in sync 11.4 seconds after it returned. | Zone-level resilience is achievable without a third copy of the data. The third zone needs to host only a Keeper member. |
| 2 | Keeper distance is paid on every write. At 100 ms round-trip time, ordinary inserts were 7.3 times slower and quorum inserts 37.7 times slower. Reads did not change. | Keeper placement is an architecture decision, not an infrastructure detail. Cross-region Keeper quorums need a business case. |
| 3 | A full patch upgrade, one server at a time, completed with zero failed reads or writes. The condition was that clients could fail over between all nodes. | Upgrades can be routine rather than change-board events, provided client failover and health-gated automation are in place. |
| 4 | Losing both replicas of one shard stopped queries by default. With partial results enabled, the cluster returned 78.7% of the rows and reported no error. | A permissive setting can turn an outage into silently wrong numbers. Policy should forbid it for financial reporting. |
| 5 | During the shard outage, inserts through the Distributed table were accepted and queued on a single node’s disk until the shard returned. | Recovery point objectives must account for data held on one server, which no replica protects. |
The common thread is that ClickHouse already provides the mechanisms for zone-level availability. The gaps we find are in how those mechanisms are placed, configured and rehearsed. That is where this paper focuses.
Reference architecture
The ClickHouse high availability architecture we recommend
The architecture in Exhibit 1 follows from a single principle: no failure domain may hold both copies of a shard or a majority of Keeper votes. With three ClickHouse availability zones, that means one replica of every shard in each of two data zones, and one Keeper member in each of the three zones. The third zone stores no table data. Its only role is to provide the deciding vote, so it can be a small zone or a modest instance in another region.

The cluster definition encodes the placement. Each shard lists one replica per data zone, and internal_replication leaves copying to ReplicatedMergeTree rather than to the Distributed engine. Macros carry the shard and replica identity, so the table DDL is identical on every server. That uniformity is what makes servers interchangeable during upgrades and rebuilds.
<!-- config.d/cluster.xml: ClickHouse high availability architecture, two shards across two data zones -->
<clickhouse>
<remote_servers>
<prod>
<shard>
<internal_replication>true</internal_replication> <!-- replicas copy via Keeper log -->
<replica><host>ch-s1r1.zone-a</host><port>9000</port></replica>
<replica><host>ch-s1r2.zone-b</host><port>9000</port></replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica><host>ch-s2r1.zone-b</host><port>9000</port></replica>
<replica><host>ch-s2r2.zone-a</host><port>9000</port></replica>
</shard>
</prod>
</remote_servers>
<!-- per server: these three values are the only difference between nodes in ClickHouse availability zones A and B -->
<macros>
<cluster>prod</cluster>
<shard>01</shard>
<replica>r1</replica>
</macros>
<!-- ClickHouse Keeper quorum: one member per zone, A, B and the witness zone C -->
<zookeeper>
<node><host>keeper1.zone-a</host><port>9181</port></node>
<node><host>keeper2.zone-b</host><port>9181</port></node>
<node><host>keeper3.zone-c</host><port>9181</port></node>
<session_timeout_ms>30000</session_timeout_ms>
<operation_timeout_ms>10000</operation_timeout_ms>
</zookeeper>
</clickhouse>-- ClickHouse high availability architecture: one logical table, two shards, two replicas each
CREATE DATABASE IF NOT EXISTS pay ON CLUSTER prod;
CREATE TABLE IF NOT EXISTS pay.txn_local ON CLUSTER prod
(
txn_id UInt64,
merchant_id UInt32,
created_at DateTime64(3, 'UTC'),
amount Decimal(18, 2),
currency LowCardinality(String),
status LowCardinality(String)
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{cluster}/{shard}/pay/txn_local', -- one Keeper path per shard
'{replica}' -- one entry per replica under it
)
PARTITION BY toYYYYMM(created_at)
ORDER BY (merchant_id, created_at, txn_id)
SETTINGS index_granularity = 8192;
-- The entry point applications use; rows are routed to a shard by merchant
CREATE TABLE IF NOT EXISTS pay.txn ON CLUSTER prod AS pay.txn_local
ENGINE = Distributed('prod', 'pay', 'txn_local', cityHash64(merchant_id));Replication between the two data zones is asynchronous by default. An insert is acknowledged once the receiving replica has written the part and recorded it in Keeper. The peer replica in the other zone fetches it moments later. For most analytical data that is the right trade. Where an acknowledged row must exist in both zones before the client is told it is safe, quorum inserts close the gap, at a cost that depends entirely on Keeper placement.
Coordination
Where the ClickHouse Keeper quorum lives determines write latency across ClickHouse availability zones
The ClickHouse Keeper quorum holds the replication log, the list of active replicas and the hashes used for deduplication. It does not hold data, but every insert into a replicated table has to commit to it. To size that cost we put a TCP delay proxy between the ClickHouse servers and Keeper. We then measured insert latency at round-trip times that correspond to the same host, the same zone, distant zones, another region and another continent.

Two patterns emerge. Ordinary inserts grew roughly in step with distance: about 2.6 round trips per insert, from 42 ms locally to 306 ms at 100 ms RTT. Quorum inserts grew far faster, from 76 ms to 2,844 ms. The second replica has to learn about the part, fetch it and confirm it through Keeper before the client is answered, so each of those steps pays the distance again. Reads did not change at all, at 5 to 9 ms, because SELECTs on replicated tables never consult Keeper.
The query log shows the same effect from the server’s side. On the receiving replica, a quorum insert issued more than five times as many Keeper transactions as an ordinary one:
-- ClickHouse Keeper quorum cost per insert, from the receiving replica's query log
SELECT
Settings['insert_quorum'] AS quorum, -- '' means the default, 0
count() AS inserts,
round(quantile(0.5)(query_duration_ms)) AS p50_ms,
round(avg(ProfileEvents['ZooKeeperTransactions']), 1) AS keeper_txns_per_insert
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_kind = 'Insert'
AND has(tables, 'pay.txn_local')
AND event_date = today()
GROUP BY quorum
ORDER BY quorum;
-- quorum inserts p50_ms keeper_txns_per_insert
-- 980 72 1.5
-- 2 200 440 8The design consequence is clear. Keep the Keeper quorum within single-digit milliseconds of the servers that write: one region, spread across zones. A cross-region Keeper ensemble is sometimes proposed as a disaster recovery measure. In practice it charges a latency tax on every write, every day, to protect against an event that a separate DR cluster handles better. Quorum inserts belong on the narrow set of tables where a row acknowledged in one zone must already exist in the other.
<!-- keeper.xml on each member: a three-zone ClickHouse Keeper quorum (ids 1, 2, 3 = zones A, B, C) -->
<clickhouse>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id> <!-- 2 in zone B, 3 in zone C -->
<log_storage_path>/var/lib/clickhouse-keeper/log</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse-keeper/snapshots</snapshot_storage_path>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<session_timeout_ms>30000</session_timeout_ms>
</coordination_settings>
<raft_configuration>
<server><id>1</id><hostname>keeper1.zone-a</hostname><port>9234</port></server>
<server><id>2</id><hostname>keeper2.zone-b</hostname><port>9234</port></server>
<server><id>3</id><hostname>keeper3.zone-c</hostname><port>9234</port></server>
</raft_configuration>
</keeper_server>
</clickhouse>Change without downtime
A ClickHouse rolling upgrade with zero failed requests on 26.9
Most production downtime on mature clusters is planned: upgrades, configuration changes and host maintenance. That makes the ClickHouse rolling upgrade the most frequently exercised part of any ClickHouse high availability architecture. We started the cluster on ClickHouse 26.9.2.8 and upgraded it to 26.9.17.8 one server at a time, while a client wrote and read every 100 ms. The client failed over between all four servers, as a load balancer health-checking each node would. The upgrade order took zone B first, then zone A, so that each shard always kept one replica serving.

All 503 writes and 651 reads succeeded. 57 writes and 77 reads were retried on a second node, which is the client failover doing its job. Each server answered queries 2.4 to 3.3 seconds after starting and caught up within a further 1.3 to 2.6 seconds. One stop took 30.14 seconds rather than under a second: the server waited for six open connections to drain before shutting down. Change windows should allow for that, and load balancers should drain a node before it is stopped.
We automate the procedure with a gate before and after each server. The gate refuses to touch a node whose replicas are unhealthy, or whose shard peers are not active, because restarting it would take a shard offline. The script below ran unchanged against the lab cluster.
#!/usr/bin/env bash
# ClickHouse rolling upgrade / restart for a ClickHouse high availability architecture, one node at a time, with a gate before and after each node.
# Usage: rolling_restart.sh <node-host:tcp-port> <restart-command>
# Blast radius: one replica of one shard at a time. Rollback: restart the node on the previous binary.
set -euo pipefail
NODE="$1"; RESTART_CMD="$2"
HOST="${NODE%:*}"; PORT="${NODE#*:}"
CH="${CH_CLIENT:-clickhouse-client} --host ${HOST} --port ${PORT} --user ${CH_USER:-default}"
[ -n "${CH_PASSWORD:-}" ] && CH="${CH} --password ${CH_PASSWORD}" # never hard-code the secret
# Gate 1: every replicated table on this node must be healthy and caught up before we touch it,
# and its peer replicas must be active, otherwise this restart takes a shard offline.
UNHEALTHY=$($CH -q "SELECT count() FROM system.replicas
WHERE is_readonly OR is_session_expired OR queue_size > 0 OR active_replicas < total_replicas")
if [ "${UNHEALTHY}" != "0" ]; then
echo "ABORT: ${UNHEALTHY} replicated tables not healthy on ${NODE}; fix before restarting"; exit 1
fi
BEFORE=$($CH -q "SELECT version()")
echo "$(date -u +%T) ${NODE} healthy on ${BEFORE}, restarting"
# Restart (graceful stop drains connections; start may switch binary for an upgrade)
eval "${RESTART_CMD}" # e.g. "systemctl restart clickhouse-server"
# Gate 2: wait until the node answers, then until every replica queue is empty again
until $CH -q "SELECT 1" >/dev/null 2>&1; do sleep 1; done
until [ "$($CH -q "SELECT count() FROM system.replicas WHERE is_readonly OR queue_size > 0")" = "0" ]; do sleep 1; done
$CH -q "SYSTEM SYNC REPLICA pay.txn_local LIGHTWEIGHT" # wait for the fetches already queued for this replica
echo "$(date -u +%T) ${NODE} back on $($CH -q "SELECT version()"), queues empty; safe to continue"
# Lab output for one node:
# 19:54:27 127.0.0.1:9502 healthy on 26.9.17.8, restarting
# 19:54:36 127.0.0.1:9502 back on 26.9.17.8, queues empty; safe to continueTwo controls make this safe at scale. Upgrade Keeper members separately from ClickHouse servers, one at a time, and never while a server upgrade is in progress. And hold each new version on one replica per shard for a soak period before the second replica follows. Patch releases on the 26.9 line have shipped almost daily in October, so the soak, not the mechanics, is the real governance decision.
Failure scope
ClickHouse disaster recovery: what a zone, a shard and a queue cost
For ClickHouse disaster recovery planning, we then removed larger pieces of the cluster under the same live traffic. Exhibit 4 sets out the results, with the exposure each scenario leaves behind.

Zone loss. We stopped one of the two data-bearing ClickHouse availability zones, zone B, outright: two ClickHouse servers and one Keeper member. Keeper retained its majority, and each shard kept its zone A replica. In the 60-second outage, 345 writes and 506 reads all succeeded, and the slowest write took 241 ms. After zone B returned, its servers had replayed their queues and were in sync 11.4 seconds later. In our run the Keeper leader happened to sit in zone C. Had it been in zone B, writes would also have paused for a leader election, which takes about a second on this ensemble.
Shard loss. With both replicas of shard 02 stopped, a default query failed with Code 279: all connection attempts failed. That is the correct behaviour. With skip_unavailable_shards = 1, the same query returned 5,489,300 of 6,976,000 rows and reported no error. A dashboard would show a 21% drop in transactions that never happened. We recommend that this setting is locked off for any role that feeds financial or regulatory reporting.
Writes during shard loss. Inserts through the Distributed table in its default background mode kept succeeding: 100,000 rows in 50 inserts, no failures. Rows for the healthy shard landed immediately. The 49,400 rows destined for shard 02 waited in 50 files on the entry node’s disk and drained 11.09 seconds after the shard returned. The final count matched the expected 7,076,000 exactly. Until it drained, though, that queue was a single copy of acknowledged data. It is an RPO exposure that a replica does not cover, and it should be monitored as such.
-- ClickHouse disaster recovery watch across ClickHouse availability zones: acknowledged rows that exist on one node only
SELECT
hostName() AS node,
database,
table,
data_files, -- batches waiting for an unreachable shard
formatReadableSize(data_compressed_bytes) AS queued,
error_count,
substring(last_exception, 1, 80) AS last_error
FROM clusterAllReplicas('prod', system.distribution_queue)
WHERE data_files > 0
ORDER BY data_compressed_bytes DESC;
-- during the shard 02 outage:
-- node database table data_files queued error_count last_error
-- ... pay txn 50 313.60 KiB 5 Code: 279. DB::NetException: All connection tries failed...Quantifying availability
An illustrative availability model for ClickHouse high availability architecture
Leadership teams usually ask for availability as a number. The model below shows how replica count and Keeper size combine, under one stated and deliberately simple assumption: each server and each Keeper member is independently available 99.5% of the time. The figures are illustrative arithmetic, not measurements and not a promise. Real outages are correlated: a zone, a deployment or an operator error takes several components at once. That is exactly why the zone-level tests above matter more than any table of nines.
# ClickHouse high availability architecture, illustrative availability arithmetic: independent failures, every component 99.5% available.
# Reads need one live replica per shard; writes also need a Keeper majority.
from math import comb
A = 0.995 # assumed availability of one server or Keeper member
def keeper_majority(n, a): # probability that a majority of n members is up
k = n // 2 + 1
return sum(comb(n, i) * a**i * (1 - a)**(n - i) for i in range(k, n + 1))
def shard_up(replicas, a): # at least one replica of the shard is up
return 1 - (1 - a) ** replicas
for shards, replicas, keepers in [(2, 1, 0), (2, 2, 3), (2, 2, 5), (2, 3, 5)]:
read = shard_up(replicas, A) ** shards
write = read * (keeper_majority(keepers, A) if keepers else 1)
print(shards, replicas, keepers,
f"read {read:.5%} ({(1 - read) * 525600:.2f} min/yr)",
f"write {write:.5%} ({(1 - write) * 525600:.2f} min/yr)")| Configuration (illustrative) | Read availability | Expected read downtime per year | Write availability | Expected write downtime per year |
|---|---|---|---|---|
| 2 shards × 1 replica, no Keeper | 99.00250% | 5,242.9 min | 99.00250% | 5,242.9 min |
| 2 shards × 2 replicas, 3 Keeper members | 99.99500% | 26.3 min | 99.98753% | 65.6 min |
| 2 shards × 2 replicas, 5 Keeper members | 99.99500% | 26.3 min | 99.99488% | 26.9 min |
| 2 shards × 3 replicas, 5 Keeper members | 99.99998% | 0.13 min | 99.99985% | 0.78 min |
Two points survive the simplifying assumption. First, a second replica buys almost two orders of magnitude of read availability, and it is the single largest improvement on the table. Second, once data is replicated, Keeper becomes the limiting factor for writes. Going from three to five members more than halves expected write downtime in this model. A five-member Keeper ensemble across three zones is laid out 2-2-1, which still leaves three of five votes when any one zone is lost.
Assessment framework
The five-layer ClickHouse disaster recovery and resilience framework we assess against
When we assess an existing ClickHouse high availability architecture, we score it against five layers, each with a question it must answer and the evidence that answers it. The ordering matters. Layers one and two determine whether the service stays up. Layers three to five determine whether an outage also loses or corrupts data.

The pattern we most often find is strong scores on topology and coordination and weak scores on change safety and recovery. The cluster is replicated and Keeper is healthy. But no one has measured what a restart costs, partial results are enabled for convenience, the Distributed queue is not monitored, and the last restore test predates the current schema. Closing those gaps is mostly configuration and rehearsal, not new infrastructure.
Recommendations
A 90-day roadmap to a tested ClickHouse high availability architecture
| Horizon | Action | Owner | Evidence of completion |
|---|---|---|---|
| First 30 days | Map every replica and Keeper member to a failure domain; correct any shard with both replicas in one zone | Platform engineering | system.clusters and macros reconciled with the zone inventory |
| First 30 days | Lock skip_unavailable_shards off for reporting roles; alert on system.distribution_queue | Database engineering | Settings profile constraint; alert firing in a test |
| Days 31 to 60 | Measure ClickHouse Keeper quorum RTT and per-insert Keeper cost; move the quorum into one region if it spans more | Platform engineering | Insert latency and Keeper transactions from system.query_log |
| Days 31 to 60 | Automate gated rolling restarts; run one ClickHouse rolling upgrade end to end under load | Database engineering | Zero failed requests in the upgrade record |
| Days 61 to 90 | Rehearse ClickHouse disaster recovery for zone loss and shard loss on staging; record RTO and RPO per scenario | SRE with application owners | Drill report signed off by the service owner |
| Days 61 to 90 | Establish backups outside the cluster and a quarterly restore test | SRE | Restored table validated by row count and checksum |
Every change in this roadmap should be applied to a staging environment first, with a written rollback, and only then promoted to production. Replication protects availability; it does not replace backups. A bad mutation or an accidental TRUNCATE replicates to every replica in seconds.
Risk register
ClickHouse high availability architecture: the residual risks we see most often
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Both replicas of a shard in one of the ClickHouse availability zones after a host replacement | Medium | Zone loss becomes shard loss | Placement check in the provisioning pipeline |
| ClickHouse Keeper quorum stretched across regions | Low | Every write slowed, by up to 37.7× for quorum inserts in our tests | Single-region quorum; separate DR cluster |
| Partial results enabled for reporting users | Medium | Silently wrong figures during a shard outage | Profile constraint; query audit |
| Distributed queue on a single node during shard loss | Medium | Loss of acknowledged rows if that node fails | Queue alerting; foreground inserts for critical tables |
| A ClickHouse rolling upgrade applied to both replicas of a shard together | Low | Planned outage, or a bad release on every copy | Gated rolling procedure; soak on one replica |
Methodology
How we tested this ClickHouse high availability architecture, and the limits of the evidence
The test cluster ran four ClickHouse servers, two shards with two replicas each, and three ClickHouse Keeper members, all on one 2 vCPU host with 8 GiB of RAM. Zones were logical: a zone failure was emulated by stopping every process assigned to it. Keeper distance was emulated with a userspace TCP delay proxy between servers and Keeper. Raft traffic between Keeper members was not delayed. Servers started on 26.9.2.8 for the upgrade test and ran 26.9.17.8 afterwards; Keeper ran 26.9.17.8 throughout. The workload was a synthetic payments table of up to 7.1 million rows.
The evidence has clear limits. One host cannot reproduce real network partitions, packet loss, disk failure or the contention of production query load. Failure timings on real infrastructure will be longer, because detection depends on timeouts rather than refused connections. The availability model is arithmetic on an assumed component availability. We present these results as the minimum behaviour a well-configured cluster should show, and as a template for the drills every production estate should run on its own infrastructure.
FAQ
ClickHouse high availability architecture: questions we are asked
How many ClickHouse availability zones does a ClickHouse high availability architecture need?
Three, but only two need to hold data. Put one replica of every shard in each of two zones, and one ClickHouse Keeper member in each of the three. In our test that layout lost a full zone with no failed requests.
Should a ClickHouse Keeper quorum span regions, or only ClickHouse availability zones?
Rarely. At 100 ms round-trip time, inserts in our tests were 7.3 times slower and quorum inserts 37.7 times slower. Keep the quorum inside one region and handle regional disaster recovery with a separate cluster and backups.
Can a ClickHouse rolling upgrade avoid downtime?
Yes. A rolling upgrade from 26.9.2.8 to 26.9.17.8 under load had zero failed reads or writes in our test. It needs client failover across nodes, a health gate before and after each server, and one replica per shard upgraded at a time.
What is the RPO in ClickHouse disaster recovery?
For replicated tables with default settings, the window is the brief interval before a peer replica fetches a new part; quorum inserts close it. Rows queued in a Distributed table’s background queue exist on one node only until delivered. Count them in the RPO.
Is skip_unavailable_shards safe to enable?
Not for reporting. With one shard down it returned 78.7% of the rows in our test and raised no error. Keep it off for any role whose numbers drive decisions.
Further reading
Related ChistaDATA research and sources
From ChistaDATA: eight failure drills on ClickHouse 26.9, our companion study of replica and Keeper failures, and ClickHouse performance observability, for the system tables used throughout this paper.
ClickHouse documentation: data replication, ClickHouse Keeper, the Distributed table engine and the changelog.
Measurements: ChistaDATA lab, October 2026, ClickHouse 26.9.2.8 and 26.9.17.8 on one 2 vCPU host, 4 servers and 3 Keeper members, synthetic data. Availability figures are illustrative arithmetic under stated assumptions. Test every recommendation on your own infrastructure before production use.
ChistaDATA
Commission a ClickHouse high availability assessment
ChistaDATA assesses production ClickHouse estates against the five-layer framework in this paper. The assessment covers topology and failure-domain mapping, Keeper placement and latency, write-durability policy, gated rolling-upgrade automation, and zone and shard loss drills with measured RTO and RPO. We work on 100% open-source ClickHouse, with 24×7×365 support and a 15-minute Severity 1 response.
Running ClickHouse in production? ChistaDATA provides ClickHouse consulting for architecture, performance and migrations, and 24×7 ClickHouse support with a 15-minute S1 response. For day-to-day operations see ClickHouse DBA services and ClickHouse managed services.