Skill 05 · Clickhouse Js Node Coding
Subchapter 5.1
reference/async-insert.mdMarkdown4 KBView on GitHub
Applies to: all client versions; the relevant settings are server-side. See https://clickhouse.com/docs/en/optimize/asynchronous-inserts.
When to use async inserts: when many small inserts arrive concurrently (e.g., one per HTTP request) and you don’t want to maintain a client-side batching layer. ClickHouse will batch them server-side. This is also the recommended ingestion pattern for ClickHouse Cloud.
When not to use async inserts: when you already build large batches client-side (e.g., from a stream). Plain inserts are simpler and lower latency.
Enable on the client level or per-request via clickhouse_settings:
import { createClient, ClickHouseError } from "@clickhouse/client";
const client = createClient({
url: process.env.CLICKHOUSE_URL,
password: process.env.CLICKHOUSE_PASSWORD,
max_open_connections: 10,
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1, // wait for ack from server
async_insert_max_data_size: "1000000",
async_insert_busy_timeout_ms: 1000,
},
});Each call still uses the client’s normal insert() API — the server merges
the batches.
const promises = [...new Array(10)].map(async () => {
const values = [...new Array(1000).keys()].map(() => ({
id: Math.floor(Math.random() * 100_000) + 1,
data: Math.random().toString(36).slice(2),
}));
await client
.insert({ table: "async_insert_example", values, format: "JSONEachRow" })
.catch((err) => {
if (err instanceof ClickHouseError) {
// err.code matches a row in system.errors
console.error(`ClickHouse error ${err.code}:`, err);
return;
}
console.error("Insert failed:", err);
});
});
await Promise.all(promises);wait_for_async_insert | Promise resolves when… | Trade-off |
|---|---|---|
1 (default) | Server has flushed the batch to the table | Slower per call; insert errors surface to the client |
0 | Server accepted the row into its in-memory buffer | Faster; flush errors won’t surface — only validation/parsing errors |
With wait_for_async_insert: 1, expect each insert call to take roughly
async_insert_busy_timeout_ms to resolve when traffic is light, because the
server waits for more rows or for the timer to fire before flushing.
When creating tables in scripts that immediately insert, ack the DDL with
wait_end_of_query: 1 so the table is ready before the first insert:
await client.command({
query: `
CREATE OR REPLACE TABLE async_insert_example (id Int32, data String)
ENGINE MergeTree ORDER BY id
`,
clickhouse_settings: { wait_end_of_query: 1 },
});Even better is to create a specialized client for inserts with the appropriate async settings and a separate client for DDL and other queries.
async_insert per call but expecting client-side batching.
The client still issues each insert() as a separate HTTP request — the
batching happens on the server.wait_for_async_insert (async-insert ack) with
wait_end_of_query (DDL ack). They are unrelated.wait_for_async_insert: 0 as
durably written. It only means the server accepted the bytes; flush
failures will not surface to the client.ClickHouseError. It exposes err.code, which maps to
rows in the system.errors table — use it to decide whether to retry.examples/node/performance/ (opens in a new tab).