calricon.

General

The ClickHouse "Too Many Parts" Crisis: Why Streaming Micro-Batch Inserts Silently Paralyze MergeTree Engines

Oct 22, 2026/7 min read

calricon / field note

When engineering teams treat ClickHouse like an OLTP database by streaming low-latency micro-batch inserts, the MergeTree engine collapses under the dreaded "Too many parts (300)" exception. Discover why background merges fail to keep pace and how elite data architects engineer deterministic high-throughput ingestion.

Oct 22, 2026/General
222
Advertisement

The ClickHouse "Too Many Parts" Crisis: Why Streaming Micro-Batch Inserts Silently Paralyze MergeTree Engines

In real-time analytical data platforms, ClickHouse is widely celebrated for executing sub-second aggregations across billions of rows. Eager to achieve instantaneous observability, fraud detection, or event analytics, engineering teams frequently connect streaming Kafka consumers, microservice API logging hooks, or event dispatchers directly to ClickHouse tables. Operating under the familiar mental model of relational databases (where single-row or tiny micro-batch INSERT statements are routine), the application streams hundreds of concurrent writes per second into the database. Within hours, the ingestion pipeline crashes into an impenetrable brick wall: the database rejects all incoming write transactions with the fatal error "Code: 252. DB::Exception: Too many parts (300) in all data parts in table ... Merges are processing significantly slower than inserts." The write pipeline halts, upstream message queues overflow, and real-time dashboards go completely dark.

ClickHouse isn't Postgres. Every single INSERT statement creates a brand-new immutable directory on disk. If you stream hundreds of tiny writes per second, your background merge threads can't physically keep up—and ClickHouse will slam the emergency brake on your entire write pipeline.

The Physics of MergeTree: Why Every INSERT Creates a File System Landmine

To understand why ClickHouse chokes on streaming writes, engineers must discard the row-store assumptions of PostgreSQL and MySQL and confront the physical architecture of the MergeTree storage engine.

In ClickHouse, data is not appended to a single, monolithic open file. Instead, the storage architecture adheres to a strict, immutable part model:

  1. Atomic Part Creation: Every single INSERT statement executed against a MergeTree table—whether it contains one row or one million rows—creates a completely separate, immutable physical directory on disk containing columnar data files (.bin) and primary index marks (.mrk). This directory is called a "data part."
  2. The Asynchronous Merge Cycle: To keep query scans fast, a background maintenance thread pool continuously identifies small adjacent parts and combines them into larger, compressed, and sorted parts via asynchronous merges.
  3. The Geometric Imbalance: A single background merge operation requires reading multiple parts from NVMe storage, decompressing column blocks, sorting records according to the primary key, re-compressing them, and writing a new part to disk. If your application sends 200 small INSERT statements per second, ClickHouse is forced to create 200 physical disk directories per second—a rate that mathematically outpaces the physical disk I/O and CPU throughput of any background merge thread pool.

To prevent the filesystem from collapsing under millions of tiny files and to protect query scan performance from suffocating under thousands of unmerged parts, ClickHouse enforces an emergency hard safety brake: parts_to_throw_insert (defaulting to 300 parts per partition). When the number of unmerged active parts in any single partition hits 300, ClickHouse instantly aborts all write transactions with Exception 252.

The Three Architectural Sins That Guarantee Part Saturation

1. Direct Microservice Ingestion (The OLTP Delusion)

The most ubiquitous root cause is microservices writing directly to ClickHouse. An API gateway or user tracking service receives an HTTP POST request and executes:

INSERT INTO events VALUES ('user_123', 'click', now());

Even if throughput is modest (e.g., 50 requests per second), ClickHouse generates 3,000 distinct data parts every single minute. Within minutes, the merge engine falls hopelessly behind, and the table locks out all writes.

2. Catastrophic Partitioning Granularity (Over-Partitioning)

ClickHouse tables partition data according to the PARTITION BY expression. A common beginner blunder is partitioning by excessive temporal or categorical granularity:

PARTITION BY (toYYYYMMDD(event_time), user_id)
-- or:
PARTITION BY toStartOfHour(event_time)

When a table is partitioned by hour or user ID, incoming writes are scattered across dozens or hundreds of distinct partitions simultaneously. Because ClickHouse merges parts strictly within the boundary of a single partition—and cannot merge parts across different partitions—each active partition maintains its own independent part counter. A modest write stream scattered across 50 hourly partitions multiplies the active part footprint by 50x, triggering Exception 252 across multiple partitions simultaneously.

3. Kafka Engine Misconfigurations

Teams frequently utilize the native Kafka table engine paired with a Materialized View to ingest events. However, if the Kafka consumer table is configured with tiny poll timeouts (stream_poll_timeout_ms = 100) or tiny batch sizes without proper buffering thresholds, the consumer commits micro-batches containing only 10 to 50 messages, hammering the target MergeTree table with hundreds of tiny writes per second.

Naive Streaming Ingestion vs. Calricon High-Throughput Batch Architecture

Evaluation DimensionNaive Streaming ModelCalricon Production Standard
Write CadenceContinuous streaming (10 to 500 INSERTs/sec)Metered batching (1 INSERT every 1 to 5 seconds)
Rows per INSERT Statement1 to 100 rows per transaction10,000 to 100,000 rows per transaction
Disk Part Generation Rate3,000 to 30,000 unmerged parts per hour720 to 3,600 large, pre-coalesced parts per hour
Background Merge Load100% disk I/O saturation; merges fail to keep pace<15% background disk I/O; linear compaction
Write Rejection ResilienceHigh vulnerability to Exception 252 (Too many parts)Absolute immunity; parts count maintained <30

The Calricon Production Playbook: Taming the MergeTree Engine

Eliminating the "Too many parts" crisis requires shifting ingestion from an unmetered push model to a disciplined, batched architecture.

Step 1: Leverage Native Asynchronous Inserts (async_insert)

Modern ClickHouse (v21.11+) includes native server-side batching via async_insert. Instead of writing a new part immediately upon receiving an INSERT, ClickHouse buffers incoming small inserts in server RAM and flushes them into a single, optimized data part once a configured size or time threshold is reached:

-- Enable server-side asynchronous batching in users.xml or per-session
SET async_insert = 1;
SET wait_for_async_insert = 1;
SET async_insert_busy_timeout_ms = 1000;
SET async_insert_max_data_size = 10485760; -- 10MB batch buffer

When wait_for_async_insert = 1 is enabled, the client connection blocks until the batch is flushed to disk, ensuring client-side backpressure and data durability without creating millions of tiny parts.

Step 2: Enforce Coarse Temporal Partitioning

Audit your PARTITION BY expressions today. Never partition by day, hour, or high-cardinality keys unless retention policies strictly mandate dropping full days at a time. The gold standard for enterprise ClickHouse tables is monthly partitioning:

CREATE TABLE production_events (
    event_id UUID,
    event_time DateTime64(3),
    user_id UInt64,
    payload String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (user_id, event_time);

Monthly partitioning consolidates incoming writes into one or two active partitions (current month and prior month transitions), allowing background merges to operate with maximum efficiency.

Step 3: Deploy Client-Side Buffer Collectors (Vector / FluentBit)

For microservice architectures generating millions of logs and metrics, do not allow application pods to connect directly to ClickHouse. Interpose a lightweight, high-performance edge collector (such as Vector or FluentBit). Configure the collector to accumulate records in memory and dispatch bulk batches:

# Example: Vector ClickHouse Sink Configuration
[sinks.clickhouse_events]
type = "clickhouse"
inputs = ["transform_events"]
endpoint = "http://clickhouse-cluster:8123"
database = "analytics"
table = "production_events"
batch.max_bytes = 10485760   # 10 MB batches
batch.timeout_secs = 2       # Flush every 2 seconds maximum

Step 4: Instrument Prometheus Alerts on Active Part Ratios

Never wait for ClickHouse to throw Exception 252. Monitor active part counts per partition using system.parts:

- alert: ClickHousePartsPerPartitionApproachingLimit
  expr: max(count() by (table, partition, instance)) by (table, instance) from system.parts where active = 1 > 200
  for: 3m
  labels:
    severity: warning
  annotations:
    summary: "ClickHouse table {{ $labels.table }} on {{ $labels.instance }} has over 200 active parts in a partition; approaching 300 limit."

- alert: ClickHouseMergeSpeedLagging
  expr: rate(ClickHouseProfileEvents_MergedRows[5m]) < rate(ClickHouseProfileEvents_InsertedRows[5m])
  for: 10m
  labels:
    severity: critical
  annotations:
    summary: "ClickHouse ingestion rate exceeds background merge compaction throughput."

Stop Treating Analytical Engines Like Transactional Sinks

ClickHouse is a Formula 1 racing engine engineered to execute complex vector math across columnar data at memory-bus speeds. But when you feed it a continuous stream of single-row writes, you are forcing that racing engine to drive through a congested parking lot.

Audit your ingestion pipelines today. Turn on async_insert, replace fine-grained daily partitions with monthly boundaries, interpose batching buffers between your microservices and the database, and monitor your active part ceilings. In analytical data architecture, speed is not achieved by writing instantly; it is unlocked by writing in disciplined, massive, and unstoppable waves.

Advertisement
Talk to Calricon