What one machine measured.
Latency and throughput for a produce, memory per broker, and the costs that changed in the last round of performance work. Each table compares the newest run with an earlier one. The full output is in the Benchmarks README.
Setup
2026-11-10, commit 56f0c3a. Apple M4 Pro, macOS, APFS on the internal SSD. Release build. MinIO in Docker on the same machine. The machine was otherwise idle, and each benchmark ran once.
The baseline is 2026-11-05, commit 97cd9f3, on the same machine. All brokers share one SSD, and on macOS fsync is a full device flush. Numbers with three brokers are pessimistic for that reason.
What an acknowledgement is
A producer sends a batch and waits for the broker to answer. That answer is the acknowledgement, or ack. It is the broker's promise that the batch is safe, and it is what every latency below measures: the time from sending a batch to its ack. A producer asks for a promise with the acks setting:
| Setting | Kafka acknowledges when | Camus acknowledges when |
|---|---|---|
acks=0 | Never. The producer does not wait. | Never. The batch may be lost. Not benchmarked. |
acks=1 | The leader has written the batch. | The same moment as acks=all. |
acks=all | Every in-sync replica has the batch. | The topic's durability mode is satisfied, as below. |
So for Camus the ack depends on the topic's durability.mode, not on acks:
| Mode | The ack means | What can still be lost |
|---|---|---|
wal | A majority of the WAL shard has fsynced the batch to local disk. The upload to the bucket follows. | Acknowledged batches not uploaded yet, normally a few seconds of log at most. They are lost only if a majority of brokers lose their disks before the upload. |
r2 | The object is in the bucket, and the metadata group has committed its offsets. | Nothing acknowledged, even if every broker's disk is gone. |
That is why the two modes differ by more than 20 times in the latency table. A wal ack waits for disk flushes on a majority of brokers. An r2 ack waits for an object store put and a metadata commit. The fsyncs per ack column in the throughput table shows how many acks share one disk flush. Under load, one flush answers many producers.
Three brokers, one producer, 1 KiB records, acks=all.
Consumers acknowledge too, in a different sense. A consumer group commits the offset it has processed up to, and that commit is a metadata write. The metadata writes row near the end measures it. The ack to a waiting consumer row measures something else: how soon after a producer's ack a consumer sees the batch.
Latency, 3 brokers
One producer sends 1 KiB records with acks=all and waits for each acknowledgement before sending the next. wal answers after a disk majority has the batch. r2 answers after the object is stored. See durability.
| Mode | Samples | p50 ms | p99 ms | max ms | Baseline p50 / p99 / max |
|---|---|---|---|---|---|
wal | 2000 | 11.11 | 27.07 | 42.96 | 11.06 / 31.06 / 38.03 |
r2 | 100 | 265.82 | 280.20 | 284.94 | 278.40 / 284.75 / 285.28 |
Throughput
Each producer sends batches of 16 records of 1 KiB with acks=all for 10 seconds. The 3-broker runs use one WAL shard. fsyncs per ack shows how many acknowledgements share one disk flush. Lower means more sharing.
| Brokers | Producers | MiB/s | p50 ms | p99 ms | fsyncs/ack | Baseline MiB/s | Baseline p50 / p99 | Baseline fsyncs/ack |
|---|---|---|---|---|---|---|---|---|
| 3 | 1 | 1.3 | 11.13 | 28.96 | 1.03 | 1.3 | 11.06 / 32.21 | 1.03 |
| 3 | 8 | 4.1 | 29.17 | 49.03 | 0.39 | 4.8 | 24.06 / 51.47 | 0.26 |
| 3 | 32 | 15.6 | 30.92 | 51.96 | 0.10 | 15.3 | 30.05 / 60.23 | 0.06 |
| 1 | 1 | 3.8 | 4.00 | 10.08 | 1.01 | 2.6 | 5.65 / 12.53 | 1.02 |
| 1 | 8 | 9.2 | 14.06 | 24.06 | 0.43 | 12.8 | 9.07 / 20.84 | 0.25 |
| 1 | 32 | 27.7 | 17.98 | 28.23 | 0.12 | 42.2 | 11.09 / 22.60 | 0.06 |
Three runs are slower than the baseline: 1 broker with 8 and with 32 producers, and 3 brokers with 8 producers. That drop is older than the work measured below. Main just before that work, commit 9687151, measured the same on 1 broker: 8.9 and 9.4 MiB/s with 8 producers, 27.2 and 28.6 MiB/s with 32, with the same fsyncs per ack. The change came in between the baseline and #93, and it is being bisected in #129.
Memory
Resident memory of the broker serving produce, from ps. Peak is the highest reading, sampled every 20 ms. After is the mean over 2 seconds, starting 3 seconds after the load stopped.
| Run | Peak MiB | After MiB | Baseline peak MiB |
|---|---|---|---|
Latency wal, 3 brokers | 29.3 | 29.3 | 30.8 |
Latency r2, 3 brokers | 22.4 | 22.6 | |
| 3 brokers, 1 producer | 37.2 | 29.9 | |
| 3 brokers, 8 producers | 41.6 | 47.0 | |
| 3 brokers, 32 producers | 177.7 | 152.8 | 232.1 |
| 1 broker, 1 producer | 45.2 | 34.5 | |
| 1 broker, 8 producers | 148.2 | 151.5 | 202.4 |
| 1 broker, 32 producers | 273.6 | 263.2 | 652.0 |
Idle was 20.7 MiB in the wal latency run. Followers peaked at 36.7 to 38.4 MiB in the heaviest 3-broker run.
Costs changed in #117 to #128
These benchmarks were added with the changes they measure. Each "before" comes from that pull request's own run, on a different day, so read the comparison as indicative.
| Benchmark | Now | Before |
|---|---|---|
| Produce to 8 partitions in one request, 1 broker, p50 / p99 | 4.04 / 10.99 ms | 34.26 / 49.95 ms |
| Ack to a waiting consumer, p50 / p90 / p99 / max | 0.18 / 0.31 / 1.56 / 4.00 ms | 11.35 / 19.68 / 22.34 / 22.60 ms |
| One connection, 5 produces in flight | 572 acks/s | about 225 acks/s |
| Metadata writes, 64 concurrent writers | 5349 writes/s | about 266 writes/s |
| 3000 sealed extents: partition metadata row | 130 bytes | 413 KB |
| 3000 sealed extents: fetch the newest, p50 / p99 | 0.15 / 0.35 ms | 57.09 / 72.76 ms |
| 3000 sealed extents: produce, p50 / p99 | 4.98 / 12.38 ms | |
| 3000 sealed extents: read all 3000 batches | 103 ms cold, 2 ms again | 259 ms cold, 261 ms again |
Log scale, so the 3,177× row doesn't flatten the rest: equal lengths are equal multiples. For times and sizes the factor is before ÷ now; for rates it is now ÷ before.
What each one measures
- 8 partitions in one request. One produce request carries a batch for each of 8 partitions, as a client batching for one broker does. Before is from #117.
- Ack to a waiting consumer. A consumer long-polls at the end of the log while a producer sends one batch every 50 ms. Each of 200 samples is the time from the produce's acknowledgement to the fetch returning that batch. Before is from #122.
- 5 produces in flight. One connection keeps five
acks=allproduces outstanding, as a Kafka client does by default. Before is from #123. - Metadata writes. 64 writers make 200 writes each to the metadata group, the way consumer groups committing offsets do. Before is from #127.
- 3000 sealed extents. Uploads run every millisecond, so each batch becomes its own sealed extent. Then it measures the partition's metadata row, a fetch of the newest extent, a produce, and a consumer reading all 3000 batches from the start, then again from the local cache. Before is from #119 for the row and the fetch, and #125 for the full read.
Linux
Some of these were also run on Linux, in an OrbStack VM on the same Mac. Those results are in the descriptions of #118 and #120. They are useful only as before and after comparisons, so they are not in these tables.
Run them yourself
Every benchmark is an ignored test, run in release mode. The ones with CAMUS_MINIO=1 need the local bucket from the bucket page.
CAMUS_MINIO=1 cargo test --release -p camus-broker --test latency -- --ignored --nocapture
CAMUS_MINIO=1 cargo test --release -p camus-broker --test throughput -- --ignored --nocapture
CAMUS_BENCH_BROKERS=1 CAMUS_MINIO=1 cargo test --release -p camus-broker --test throughput -- --ignored --nocapture
CAMUS_MINIO=1 CAMUS_BENCH_BROKERS=1 CAMUS_BENCH_PARTITIONS=8 cargo test --release -p camus-broker --test latency -- --ignored --nocapture
CAMUS_MINIO=1 cargo test --release -p camus-broker --test extent_cost -- --ignored --nocapture
cargo test --release -p camus-broker --test tail_latency -- --ignored --nocapture
cargo test --release -p camus-broker --test pipelined_throughput -- --ignored --nocapture
cargo test --release -p camus-metadata --test write_throughput -- --ignored --nocapture