learning.lab
Ingestion & Other Engines· 20/33

Async Inserts & Batching

Why tiny inserts hurt, how to fix it

Parts & Background Merges already covers why small, frequent inserts are dangerous — each one creates a new part, and enough of them outrun the background merge process. Batching client-side is the ideal fix. This page is for when you don't control the client: many independent application servers, each with a handful of rows to insert, with no good place to accumulate a batch before sending it.

Option one: the Buffer engine

A Buffer table sits in front of a real MergeTree table. Inserts land in memory, and the buffer flushes into the underlying table once row-count, byte-size, or time thresholds are crossed — turning many tiny inserts into far fewer, larger ones before they ever become a part.

CREATE TABLE events_buffer AS events
ENGINE = Buffer(
    currentDatabase(), events,   -- target database, target table
    16,                          -- number of buffer layers
    10, 100,                     -- min/max seconds before flush
    10000, 1000000,              -- min/max rows before flush
    10000000, 100000000          -- min/max bytes before flush
);

-- application inserts here, not into 'events' directly
INSERT INTO events_buffer VALUES (...);

It's the older mechanism, still used, but with a real cost: the buffer lives in server memory and is lost on restart or crash — anything not yet flushed is gone.

Option two: async_insert (the modern default choice)

Rather than a separate table, async_insert is a setting that changes how INSERT itself behaves: the server accepts the (possibly tiny) insert immediately, holds it in an internal buffer alongside inserts from other connections, and flushes the accumulated buffer as one real part once a size or time threshold is hit.

many small inserts, one real part
Many clientssmall inserts each
Server-side bufferasync_insert=1
One new partwritten to disk
Threshold hitsize or time
INSERT INTO events
SETTINGS async_insert = 1, wait_for_async_insert = 1
VALUES (102, 'click', now());

wait_for_async_insert is the trade you're actually making explicit:

  • = 1 (default) — the client's INSERT blocks until the buffered data is actually flushed to disk. Safer, but the client still waits roughly as long as it would have without async insert.
  • = 0 — the server acknowledges the insert as soon as it's in the in-memory buffer, before it's durable. Much lower client-perceived latency, at the cost of a real window where an acknowledged insert can be lost if the server crashes before flushing.
Why it exists
The real difference from client-side batching: async_insert lets ClickHouse do the batching itself, across many separate connections it doesn't control the contents of. Client-side batching is still strictly better when it's possible — it costs nothing in durability — but async_insert is the right tool when hundreds of independent services are each inserting a few rows at a time and can't coordinate a shared batch.
Production note
async_insert buffers in server memory, same as Buffer tables — high insert concurrency with wait_for_async_insert = 0 under a server crash can lose the most recently buffered, unflushed rows. Choose it deliberately for high-volume, loss-tolerant telemetry, not for data you cannot afford to lose a few seconds of.