Skip to main content

Chunk sizing and queue capacity

The chain terminal seals a chunk once its encoded buffer reaches ChunkConfig::target_bytes (default 64 KiB) and hands it to a per-shard bounded queue (SinkOptions::queue_capacity, default 8). Neither knob bounds insert size — shard workers merge chunks into full batches. What they govern is handoff rate and how long a chunk waits before a worker sees it.

This page exists because both defaults look small, and the obvious instinct is to raise them. The measurements say: raise the chunk target only when you have a specific symptom, and leave queue capacity alone.

Why the earlier thread-scaling curves were misleading

Framework overhead reports a synthetic ceiling that peaks at 2 pipeline threads, and Sink saturation peaks at 4. Neither is a framework scaling limit: both rigs swept THREADS while holding egress width fixedpipeline_synthetic hardcoded 2 I/O workers, ch_sink_saturation pinned shards and io_threads across its sweep. Each curve peaked at roughly its own fixed egress width.

The pipeline_scaling rig sweeps threads with SHARDS and IO_THREADS scaled alongside (EGRESS=scaled) and the in-flight budget derived per arm from the sizing rule. Under those conditions the framework keeps scaling, and the chunk target decides how far.

Framework ceiling vs pipeline threads, by chunk target
Higher is better
Generator source, null writer, no broker and no server — the framework alone. Egress width scaled with thread count; in-flight budget derived per arm. Medians of 3.
1 MiB64 KiB (default)
Framework ceiling vs pipeline threads, by chunk target — Higher is better1 MiB: 50.6M/s (n=3); 64 KiB (default): 41M/s (n=3); 1 MiB: 100.6M/s (n=3); 64 KiB (default): 74.6M/s (n=3); 1 MiB: 178.7M/s (n=3); 64 KiB (default): 115.8M/s (n=3); 1 MiB: 244.6M/s (n=3); 64 KiB (default): 118.2M/s (n=3); 1 MiB: 301.9M/s (n=3); 64 KiB (default): 111.1M/s (n=3)124681 MiB: 50.6M/s (n=3)50.6M/s64 KiB (default): 41M/s (n=3)41M/s1 MiB: 100.6M/s (n=3)100.6M/s64 KiB (default): 74.6M/s (n=3)74.6M/s1 MiB: 178.7M/s (n=3)178.7M/s64 KiB (default): 115.8M/s (n=3)115.8M/s1 MiB: 244.6M/s (n=3)244.6M/s64 KiB (default): 118.2M/s (n=3)118.2M/s1 MiB: 301.9M/s (n=3)301.9M/s64 KiB (default): 111.1M/s (n=3)111.1M/s
Data table
GroupSeriesValue95% CIn
11 MiB50,596,763.11810256 records/s3
164 KiB (default)41,000,817.87046864 records/s3
21 MiB100,594,219.5774915 records/s3
264 KiB (default)74,616,405.33689356 records/s3
41 MiB178,671,815.05701533 records/s3
464 KiB (default)115,793,386.27237774 records/s3
61 MiB244,578,169.56498495 records/s3
664 KiB (default)118,191,307.34747325 records/s3
81 MiB301,920,478.855918 records/s3
864 KiB (default)111,132,771.77075565 records/s3
Apple M5 Max · commit a0a05abfa2 · 2026-07-18

At the default 64 KiB the curve flattens and then falls back, while backpressure pause events climb steeply. At 1 MiB the pauses stay at zero across the whole sweep and scaling efficiency at 8 threads is roughly double.

Backpressure pause events over the same window
Lower is better
Lower is better. The sink here is a null writer that completes instantly — every one of these pauses is the handoff rate, not sink slowness.
1 MiB64 KiB (default)
Backpressure pause events over the same window — Lower is better1 MiB: 0 events (n=3); 64 KiB (default): 2 events (n=3); 1 MiB: 0 events (n=3); 64 KiB (default): 4 events (n=3); 1 MiB: 0 events (n=3); 64 KiB (default): 16 events (n=3); 1 MiB: 0 events (n=3); 64 KiB (default): 94 events (n=3); 1 MiB: 0 events (n=3); 64 KiB (default): 167 events (n=3)124681 MiB: 0 events (n=3)0 events64 KiB (default): 2 events (n=3)2 events1 MiB: 0 events (n=3)0 events64 KiB (default): 4 events (n=3)4 events1 MiB: 0 events (n=3)0 events64 KiB (default): 16 events (n=3)16 events1 MiB: 0 events (n=3)0 events64 KiB (default): 94 events (n=3)94 events1 MiB: 0 events (n=3)0 events64 KiB (default): 167 events (n=3)167 events
Data table
GroupSeriesValue95% CIn
11 MiB0 events3
164 KiB (default)2 events3
21 MiB0 events3
264 KiB (default)4 events3
41 MiB0 events3
464 KiB (default)16 events3
61 MiB0 events3
664 KiB (default)94 events3
81 MiB0 events3
864 KiB (default)167 events3
Apple M5 Max · commit a0a05abfa2 · 2026-07-18

That second chart is the load-bearing one, because the sink is a null writer that completes instantly. There is no sink slowness available to signal. The pauses are caused purely by chunks arriving faster than the shard workers can service them — the queue-full trigger is rate-driven, not only capacity-driven.

The same lever against a real sink

The synthetic rig runs an order of magnitude above any real sink, so the question is whether the lever survives contact with one. It does, though substantially attenuated — and it is not free.

Throughput vs chunk target (ClickHouse, ENGINE=Null)
Higher is better
8 pipeline threads, queue capacity 256, in-flight budget pinned at 8 GiB so the budget is identical across arms. Medians of 3.
1 MiB (highlighted)other arms
Throughput vs chunk target (ClickHouse, ENGINE=Null) — Higher is better1 MiB: 4.9M/s (n=3); 512 KiB: 4.3M/s (n=3); 128 KiB: 3.9M/s (n=3); 256 KiB: 3.6M/s (n=3); 64 KiB (default): 3.5M/s (n=3)1 MiB512 KiB128 KiB256 KiB64 KiB (default)1 MiB: 4.9M/s (n=3)4.9M/s512 KiB: 4.3M/s (n=3)4.3M/s128 KiB: 3.9M/s (n=3)3.9M/s256 KiB: 3.6M/s (n=3)3.6M/s64 KiB (default): 3.5M/s (n=3)3.5M/s
Data table
VariantValue95% CIn
1 MiB4,855,650.854132424 rows/s3
512 KiB4,323,788.15454432 rows/s3
128 KiB3,879,192.2190376557 rows/s3
256 KiB3,635,393.6059428793 rows/s3
64 KiB (default)3,542,854.417787189 rows/s3
SYNTHETIC FIXTURE · Apple M5 Max · commit a0a05abfa2 · 2026-07-18
Flush p99 vs chunk target — the cost side
Lower is better
Same arms. Lower is better: throughput and flush latency rise together.
1 MiB (highlighted)other arms
Flush p99 vs chunk target — the cost side — Lower is better64 KiB (default): 2.39 s (n=3); 128 KiB: 2.42 s (n=3); 256 KiB: 3.52 s (n=3); 512 KiB: 3.56 s (n=3); 1 MiB: 4.40 s (n=3)64 KiB (default)128 KiB256 KiB512 KiB1 MiB64 KiB (default): 2.39 s (n=3)2.39 s128 KiB: 2.42 s (n=3)2.42 s256 KiB: 3.52 s (n=3)3.52 s512 KiB: 3.56 s (n=3)3.56 s1 MiB: 4.40 s (n=3)4.40 s
Data table
VariantValue95% CIn
64 KiB (default)2.3949999999999996 s3
128 KiB2.4166666666666665 s3
256 KiB3.5166666666666417 s3
512 KiB3.5583333333333416 s3
1 MiB4.402083333333332 s3
SYNTHETIC FIXTURE · Apple M5 Max · commit a0a05abfa2 · 2026-07-18

There is no knee. Throughput and flush latency climb in near lockstep across the whole range, so no chunk target dominates the others. The shipped 64 KiB default sits deliberately at the low-latency end. Raise it when you have the specific symptom — backpressure pauses while the sink itself is idle — and accept the latency in exchange.

Pin the budget when comparing chunk sizes

The sizing rule makes the in-flight budget a function of chunk.target_bytes, so a naive A/B that lets the budget float changes two variables at once. Doing that here inflated the apparent gain by roughly a third. Every arm above pins MAX_INFLIGHT_MB to the same value.

Queue capacity: the small default is the right one

Throughput vs queue capacity
Higher is better
8 pipeline threads at the default 64 KiB chunk target, budget pinned. Medians of 3.
8 (default) (highlighted)other arms
Throughput vs queue capacity — Higher is better1024: 3.9M/s (n=3); 256: 3.5M/s (n=3); 64: 2.8M/s (n=3); 8 (default): 2.7M/s (n=3)1024256648 (default)1024: 3.9M/s (n=3)3.9M/s256: 3.5M/s (n=3)3.5M/s64: 2.8M/s (n=3)2.8M/s8 (default): 2.7M/s (n=3)2.7M/s
Data table
VariantValue95% CIn
10243,924,085.7608097848 rows/s3
2563,542,854.417787189 rows/s3
642,795,115.7681006156 rows/s3
8 (default)2,694,910.8278902927 rows/s3
SYNTHETIC FIXTURE · Apple M5 Max · commit a0a05abfa2 · 2026-07-18
Flush p99 vs queue capacity
Lower is better
Same arms. The default holds the best p99 by an order of magnitude.
8 (default) (highlighted)other arms
Flush p99 vs queue capacity — Lower is better8 (default): 247.8 ms (n=3); 64: 1.95 s (n=3); 256: 2.39 s (n=3); 1024: 2.49 s (n=3)8 (default)6425610248 (default): 247.8 ms (n=3)247.8 ms64: 1.95 s (n=3)1.95 s256: 2.39 s (n=3)2.39 s1024: 2.49 s (n=3)2.49 s
Data table
VariantValue95% CIn
8 (default)0.2478238341968912 s3
641.952499999999997 s3
2562.3949999999999996 s3
10242.4934375000000006 s3
SYNTHETIC FIXTURE · Apple M5 Max · commit a0a05abfa2 · 2026-07-18

This one is not a smooth trade, and the shape matters more than the endpoints. Moving off the default to the next step up pays roughly an order of magnitude of flush p99 for a few percent of throughput — a clearly bad trade, because the queue bounds the latency of whatever sits in it and the first step away from a shallow queue is where almost all of that latency appears. Past that point the curve inverts: the deepest arm buys substantially more throughput for comparatively little further latency.

So the practical guidance is narrower than "keep it small":

  • The default is latency-optimal by a wide margin, and that is what Tuning means by signal, not buffer.
  • If you leave it, leave it decisively. An intermediate depth pays most of the latency cost for very little of the throughput gain.
  • The throughput on the table is not negligible — this is a real choice for a throughput-dominant pipeline, not a knob to leave alone on principle.

Reading the pause counter

etl_backpressure_pause_events_total is not on its own a misconfiguration signal. In the queue-capacity arms above, the configuration with the best latency also recorded the most pauses. Pauses mean the source was throttled; whether that is healthy depends on what was throttling it.

SymptomLikely causeLever
Pauses high, sink flush p99 low, sink not saturatedchunk handoff rateraise chunk.target_bytes
Pauses high, sink flush p99 highsink genuinely behindmore shards / inflight.max_per_shard / a faster sink
Pauses high, etl_backpressure_inflight_bytes near the capbudget undersizedapply the sizing rule
etl_queue_full_events_total is blind in these rigs

That counter is registered by ShardQueues::attach_metrics, which is only reached through the pipeline builder. Every benchmark rig assembles the runtime directly, so the series is structurally always zero there and cannot be used as a validity check. The pause counter and flush histogram are the usable signals. Pipelines built through the normal builder API do record it.

Caveats

Single 18-core machine; ClickHouse runs in a container on the same cores, so absolute rates are not a cluster result — the within-run A/B is what these arms establish. pipeline_scaling arms stay within threads + io_threads + 1 <= cores; the rig marks any arm that exceeds it OVERSUBSCRIBED in its note, and none of the published arms do. The synthetic rig's payload is low-entropy filler, which flatters compression-free paths. Reproduce with the invocations in Methodology.