IL-0288 CDN77 · 2026-03-09databaseobservability
ZooKeeperWait rose to thousands of seconds on three redundant ClickHouse instances, degrading a CDN77 data pipeline
Almost all tables in the affected ClickHouse database are time series partitioned by a time unit, and writes from thousands of client machines are batched through Buffer tables. Two engineers, working from different places, independently found the cause. One looked for client machines that could explain normal batch sizes next to a falling mean and found two machines sending batches so small and so fast that they nearly halved the average. The other saw in system.part_log a sharp rise in created parts while the total number of rows stayed roughly the same, with partitions carrying timestamps as old as four months. The two machines had been unplugged from the network for several months with no other maintenance, and their shipper processes kept running. Their only data was a local health check on a regular cadence, near zero in volume, so months of it fit in the shipper's queue; the shipper batches at log time rather than send time, so the queue held many tiny batches, and the cadence evenly covered hours and days, the two most common partitioning keys. The Buffer table did combine these batches with other traffic, but each flush then spanned a long timeline that MergeTree partitioning cut into far too many parts: a table partitioned by 5 minutes gets 288 replicated parts for each historical day. The parts were inserted, replicated and immediately scheduled for TTL deletion, and the default max_number_of_merges_with_ttl_in_pool of 2 kept them from being cleared quickly, so Keeper traffic multiplied and slowed replication, though not ingest.
Most tables eventually cleared their backlog of parts on their own. The one short table partitioned by 5 minutes did not, and after the incident CDN77 dropped and recreated it and broadened its partitioning from minutes to hours. CDN77 added rejection of out-of-range timestamps in the root MaterializedView tables, with a second materialized view recording what was rejected, and built a test with an insert whose timestamps span days or months. It decided against lowering max_partitions_per_insert_block, because that setting can refuse inserts entirely and lose data. The incident also showed that Keeper operations grow much faster than linearly with the number of parts.
- Severity
- –
- Scope
- One large ClickHouse data pipeline was degraded for over an hour, with ZooKeeper traffic multiplied and replication slowed on three redundant instances; no observable impact on internal users, customers or partners
- Users affected
- none observed
- Region
- –
- Time to detect
- 15m
- Time to resolve
- –
Symptoms
At 13:30 UTC on 9 March 2026, CDN77's data team was alerted to a sudden rise in the ZooKeeperWait metric on the ClickHouse cluster behind one of its large data pipelines. Alerting for this cluster size is set at 10 sec for the low watermark and 20 sec for the high one, and the metric reached thousands of seconds. The same rise appeared on three symmetrically redundant ClickHouse instances in different datacenters, each with its own ZooKeeper instance, and the pipeline stayed degraded for over an hour, with no observable impact on internal users, customers or partners.
The first suspect was a heavy read query stalling writes, the usual cause when ZooKeeperWaitMicroseconds climbs, especially as no configuration had changed that day. A daily data-diff job had started almost exactly when the metric went up, but within minutes its queries were light again while ZooKeeperWait stayed high, and CPU, RAM and the number of executing queries on the affected instances all looked normal, so heavy reads were ruled out.
About twenty minutes in, an engineer saw that the mean insert query size in system.query_log was falling: the log shippers seemed to send a median of around 70,000 rows per second, while ClickHouse reported an average around 40,000. The team set this aside as a side effect of Keeper load, because an overloaded ClickHouse normally shows slower Buffer engine flushes and these were not slowing. Then TTL merges rose sharply, with merge reason TTLDeleteMerge everywhere in system.part_log, and the error log repeated "Not executing log entry because 2 merges with TTL already executing." The team asked whether ttl_only_drop_parts, enabled on most of its tables, had somehow been disabled. The classic "too many parts" error never fired, because the configured limits were not reached.
Two gaps made the investigation harder. system.zookeeper_log, which would have shown Keeper traffic directly, was disabled, as it is by default in ClickHouse, because it can grow very large. The table that monitors delivery delay recorded only machines at CDN layer zero because of a leftover filter, while the machines behind the problem sat on a higher layer, so the lag was never measured precisely.
Root cause
Root cause Capacity exhaustion
- Trigger
- From 13:15 UTC two client machines that had been cut off from the network for several months came back and their log shippers sent months of queued, very small batches of health check data with old timestamps, spanning a very large time range.
Detection
An alert on the ZooKeeperWait metric fired at 13:30 UTC, fifteen minutes after the problematic inserts began at 13:15. The cluster's alerting uses a low watermark of 10 sec and a high watermark of 20 sec.
Remediation
- Dropped and recreated the one table partitioned by 5 minutes that had not cleared its backlog of parts
- Broadened that table's partitioning from minutes to hours
Action items
- Reject out-of-range timestamps in the root MaterializedView tables
- Record statistics on rejected rows in a second materialized view
- Refine the deliberately wide rejection boundaries as data on rejections comes in
- Keep a minimal test scenario with an insert query whose timestamps span days or months
- Make a range of observability improvements
Lessons
Data structure matters even in a pipeline built around volume: incoherent timestamps can destabilise it as effectively as changes in cardinality. A metric that looks odd and cannot be explained at once should not be set aside under incident pressure; the falling mean insert size was what led to the cause. Means and medians each have their place, and here the mean, not the median, exposed the problem. Partitioning has to be chosen carefully: too few partitions give oversized parts, slow mutations and costly or ineffective TTL, while too many slow column scans, add final merging to aggregating queries and overload the Keeper.
Sources
References
Related cases
IL-0379InfluxDB Cloud queries in Azure West Europe slowed and timed out as storage pods fell behind and kept restartingInfluxData · 2026same root cause · shared Database, Database replication
IL-0305Replicas of one Intercom Vitess shard hit 100% CPU and fell up to 2.2 hours behind, serving stale inbox dataIntercom · 2026same root cause · shared Database, Database replication
IL-0110A Grafana Agent pod's high memory use and crash-looping Hangfire pods drove a Kubernetes node into system OOM and NotReady, twice in one dayDFDS · 2024same root cause · shared Database, Telemetry agent
IL-0443A Repro Aurora MySQL reader restarted unexpectedly and writer insert latency rose about sevenfold for a day or moreRepro · 2019same root cause · shared Database, Database replication
IL-0334Instapaper's RDS MySQL bookmarks table hit the 2TB ext3 file size limit, inserts failed and the service went downInstapaper · 2017same root cause · shared Database, Database replication
IL-0450Oracle RAC commits behind order APIs kept waiting over 0.1 s on log file sync while average host CPU had headroomToss Securities · 2026same root cause · shared Database
IL-0553CircleCI's orchestration database filled its transaction log volume, and no workflows or jobs started for 50 minutesCircleCI · 2026same root cause · shared Database
IL-0309Preset's RDS PostgreSQL 14.7 to 17.9 upgrade in US East 1 ran out of storage and took Superset down for six hoursPreset · 2026same root cause · shared Database
IL-0435Error rates rose as Aurora MySQL killed connections and declined queries to avoid OOM while load metrics stayed flatCyberAgent · 2026same root cause · shared Database
IL-0497Buttondown's database ran out of connections twice in one day and requests failed while health checks mostly passedButtondown · 2026same root cause · shared Database
IL-0350Hosts in an IONOS TXL cluster lost connectivity when an InfiniBand fabric resweep failed on a full multicast tableIONOS Cloud · 2026same root cause · shared Database
IL-0390PostHog US Cloud event ingestion slowed to delays of up to 2 days as writes to the Persons Postgres table stalledPostHog · 2025same root cause · shared Database
IL-0100PostHog's feature flags pods were packed onto Kubernetes nodes that were too small for their CPU requests, and CPU saturation above 90% caused three outages in ten days through Redis overload, connection pool exhaustion and slow queriesPostHog · 2025same root cause · shared Database
IL-0287One overloaded shard of MongoDB's internal monitoring database delayed Atlas metrics, alerts and cluster operationsMongoDB · 2025same root cause · shared Database
IL-0557Database transaction locks piled up on Storj's US1 satellite, and uploads and downloads failed with timeouts for 2 hoursStorj · 2025same root cause · shared Database
IL-0551Bitbucket Cloud's overloaded primary database failed over, and the website, API and Pipelines were slow for almost a dayAtlassian · 2025same root cause · shared Database
IL-0298Wikimedia edits failed for 38 minutes when sessionstore Cassandra nodes in both data centres crashed on full disksWikimedia Foundation · 2025same root cause · shared Database
IL-0312Gadget's main Postgres did not start again after a restart to raise max_connections, and the platform went downGadget · 2024same root cause · shared Database
IL-0544Wasabi's database cache stopped responding when its queueing service filled up, and S3 failed in all regionsWasabi · 2024same root cause · shared Database
IL-0042Application logs stopped reaching Elasticsearch and OpenSearch for hours and were lost when OpenSearch ran out of disk spaceUK Ministry of Justice · 2024same root cause · shared Telemetry agent
IL-0447An every RDS for MySQL instance slowed badly when its gp2 EBS BurstBalance ran out and IOPS fell to the baselineevery · 2024same root cause · shared Database
IL-0375MongoDB Atlas cluster changes and alerts lagged for hours after a control plane metadata migration congested a databaseMongoDB · 2024same root cause · shared Database
IL-0550Bitbucket Cloud's database slowed until connection pools saturated and requests timed out, leaving Pipelines pendingAtlassian · 2024same root cause · shared Database
IL-0270Discord's ScyllaDB authentication cluster overloaded during an OS upgrade, and logins failed for about 50 minutesDiscord · 2023same root cause · shared Database
IL-0202A burst of queries and database writes exhausted connection pools and memory on Honeycomb's ingest hosts, and Kubernetes crash-loop back-off kept failed hosts offline so ingest capacity stayed too low to recoverHoneycomb · 2023 · sev1same root cause · shared Database
IL-0386After a move to Google Cloud, Postgres connection timeouts and memcached i/o timeouts spiked on GKE nodesincident.io · 2023same root cause · shared Database
IL-0326A retention clean-up job filled the disk of MailPace's managed Postgres, which stayed offline for just over two hoursMailPace · 2023same root cause · shared Database
IL-0043Application logs missing in Kibana after a newer Fluent Bit version changed its chunk drop strategyUK Ministry of Justice · 2023same root cause · shared Telemetry agent
IL-0546Buildkite jobs could not go to newly connected agents once agent IDs passed a 32-bit integer foreign key's limitBuildkite · 2023same root cause · shared Database
IL-0194After a kube-apiserver restart, pod LIST requests from every pod of a Vector DaemonSet nearly tripled etcd memory and restarted all master nodesPalark · 2023same root cause · shared Telemetry agent
IL-0500UToronto JupyterHub logins and server starts returned 500 errors when the hub's sqlite database disk filled up2i2c · 2022same root cause · shared Database
IL-0301Basecamp 3's primary database went into total gridlock at the top of the hour and Basecamp 3 went down twice37signals · 2022same root cause · shared Database
IL-0206Consul agents in GitLab.com's GKE pods were repeatedly OOM-killed, so the Rails pods lost database replica discovery and sent every query to the primaryGitLab · 2022same root cause · shared Database
IL-0302Peak load on GitHub's mysql1 MySQL cluster maxed out its database proxy's connections and stopped writes in March 2022GitHub · 2022same root cause · shared Database
IL-0212Many Slack users could not connect because a Vitess keyspace for channel membership was overloaded by cache-miss queriesSlack · 2022same root cause · shared Database
IL-0456Inflearn went down six times during a January 2022 free-course event as slow queries piled up on Aurora PostgreSQLInflab · 2022same root cause · shared Database
IL-0321GCP snapshots on all Patroni nodes stalled GitLab.com's primary PostgreSQL disk, Patroni failed over and 5xx spikedGitLab · 2021same root cause · shared Database
IL-0384Amazon Kinesis front-end servers in US-EAST-1 could not route requests, so Kinesis and services built on it failedAmazon Web Services · 2020same root cause · shared Telemetry agent
IL-0496Ably's Cassandra hit 100% CPU in every region and queries timed out globally, and the load returned on two more eveningsAbly · 2020same root cause · shared Database
IL-0026Kubernetes nodes hit SystemOOM because fluentd-sumologic log shipper pods had no memory limitsBlue Matador · 2019same root cause · shared Telemetry agent
IL-0037Image registry rate-limited a whole region after a crash-looping DaemonSet with imagePullPolicy Always pulled through three NAT instancesDatadog · 2019same root cause · shared Database
IL-0297Mezmo users could not sign in and log indexing lagged after the auth cache's Redis servers were OOM killedMezmo · 2019same root cause · shared Database
IL-0211A Mandrill PostgreSQL shard shut down at transaction ID wraparound and email sending was impaired for about 40 hoursMailchimp · 2019same root cause · shared Database
IL-0064CPU-heavy pods packed onto one Google Cloud node drove it to kernel panics, and the crash moved from node to node until the site was downMoonlight · 2019same root cause · shared Database
IL-0075Kubernetes API servers pegged on CPU when the cluster grew to about 290 nodes, traced to Datadog agents polling the APICivis Analytics · 2019same root cause · shared Telemetry agent
IL-0300Basecamp 3 went read-only for almost five hours when its events table reached the ceiling of 2,147,483,64737signals · 2018same root cause · shared Database
IL-0333A downsized RDS PostgreSQL database maxed out its CPU at US peak and failing ELB health checks took buildkite.com downBuildkite · 2016same root cause · shared Database
IL-0296Transloadit's us-east Redis cluster lost queued jobs and heartbeats, raising WORKER_JOB_ERROR errors for 81 customersTransloadit · 2016same root cause · shared Database replication
IL-0282DynamoDB in US-East reached about 55% request errors as storage servers overloaded its metadata serviceAmazon Web Services · 2015same root cause · shared Database
IL-0383PagerDuty's shared Cassandra cluster degraded during a repair, delaying notifications and losing incoming eventsPagerDuty · 2014 · sev1same root cause · shared Database
IL-0320Postmark's primary MySQL disk filled but MySQL kept running, so MySQL-MMM never failed over and sending was delayedPostmark · 2011same root cause · shared Database
Check your cluster for this
For AI tools and scripts
- This case as JSON: https://incidentlib-api.benchlet.com/cases/IL-0288
- The cases most like it: https://incidentlib-api.benchlet.com/cases/IL-0288/similar
- MCP server (Streamable HTTP, no key):
https://incidentlib-mcp.benchlet.com/mcp
The API needs no key. The MCP server offers search_incidents, get_incident, list_incidents, get_topic and vocab; both are explained on API and MCP.