Scorecard
| Measure | KubeMQ v3.6.22 | Apache Kafka 4.3.1 | Note |
|---|---|---|---|
| Throughput, 1 KB messages, 16 partitions, 3 replicas | |||
| Highest target rate reached, 128 KB client batches | 700k msg/s | 300k msg/s | achieved ≥ 99 % of target and producers on schedule; 100k steps |
| Highest target rate reached, 512 KB client batches | 700k msg/s | 400k msg/s | same definition |
| Next rung after the top, 128 KB client | 800k target → 745k achieved; 93.1 % of target; producers 30.2 s behind schedule | 400k target → 400k achieved; publish-delay p99 838 ms | why each ladder ended |
| End-to-end latency p50 / p99, 1 KB, 128 KB client batches | |||
| at 100k msg/s (15 min) | 2 / 4 ms | 2 / 114 ms | p99 compared; a side that did not reach the target is shown but not ranked |
| at 300k msg/s | 3 / 6 ms | 3 / 274 ms | p99 compared; a side that did not reach the target is shown but not ranked |
| at 400k msg/s | 4 / 9 ms | 20 / 625 ms (publish-delay p99 838 ms) | p99 compared; a side that did not reach the target is shown but not ranked |
| at 500k msg/s | 4 / 15 ms | — | p99 compared; a side that did not reach the target is shown but not ranked |
| at 600k msg/s | 5 / 20 ms | — | p99 compared; a side that did not reach the target is shown but not ranked |
| Publish latency p99 (producer ack), 1 KB, 128 KB client batches | |||
| at 100k msg/s | 3 ms | 114 ms | |
| at 300k msg/s | 4 ms | 275 ms | |
| at 400k msg/s | 5 ms | 631 ms | |
| Large messages, 100 KB, 512 KB client batches | |||
| Highest target rate reached | 6,000 msg/s = 614 MB/s | 4,000 msg/s = 410 MB/s | payload MB/s = rate × 0.1024 |
| Achieved at 6,000 msg/s target (614 MB/s) | 6,005 msg/s = 615 MB/s | 4,694 msg/s = 481 MB/s | disk sequential-write ceiling on this rig ≈ 820 MB/s per broker; "extra" = run past that side's ladder end |
| Achieved at 8,000 msg/s target (819 MB/s) | 7,601 msg/s = 778 MB/s | 4,738 msg/s = 485 MB/s (extra) | disk sequential-write ceiling on this rig ≈ 820 MB/s per broker; "extra" = run past that side's ladder end |
| Resilience: partition leader killed (SIGKILL) at 50k msg/s | |||
| Acknowledged messages lost | 0 | 0 | acked-ID ledger verifier beside the load |
| Longest interval with no acknowledgements | 2 s | 8 s | |
| Killed server Ready again | 13 s | 23 s | |
| All replicas back in sync | 31 s | 59 s | |
| Resources at 300k msg/s, 1 KB, 128 KB client (highest rung both reached) | |||
| Broker CPU, cores of 7 (mean per broker) | 4.3 | 2.8 | |
| Broker memory working set (cgroup, includes page cache) | 2.5 GiB | 24.6 GiB | Kafka: 6 GiB heap + page cache; KubeMQ: Go heap + page cache |
| Disk written per broker | 334 MB/s | 314 MB/s | |
| Bytes written per payload byte | 1.09 | 1.02 | write amplification incl. replication log |
| Network per broker (in + out) | 649 MB/s | 635 MB/s | |
Rate ladder, 1 KB messages
Fixed producer rate, 100k msg/s steps, 5 measured minutes per rung (15 at 100k). Each side climbed until a rung achieved less than 99 % of its target or the producers fell behind schedule (publish-delay p99 ≥ 100 ms); that rung is shown with a cross. Rungs marked "extra" were run beyond that point and are kept as data.
| Target | Broker | Achieved | Last 60 s | Backlog Δ | Pub-delay p99 | Publish p50 / p99 / p99.9 | E2E p50 / p99 / p99.9 | Disk MB/s | CPU cores | Limit seen |
|---|---|---|---|---|---|---|---|---|---|---|
| 100k | KubeMQ | 100.0k (100.0 %) | 100.0 % | -250 | 0.07 ms | 1.6 / 3 / 12 ms | 2 / 4 / 19 ms | 117 | 3.8 | none |
| Kafka | 100.0k (100.0 %) | 99.9 % | -530 | 0.07 ms | 1.6 / 114 / 246 ms | 2 / 114 / 245 ms | 104 | 2.0 | client window | |
| 200k | KubeMQ | not run: this side's ladder had ended | ||||||||
| Kafka extra | 200.2k (100.1 %) | 100.0 % | 62 | 0.07 ms | 1.9 / 233 / 353 ms | 2 / 232 / 351 ms | 206 | 2.4 | client window | |
| 300k | KubeMQ | 300.3k (100.1 %) | 100.0 % | 11 | 0.07 ms | 2.0 / 4 / 7 ms | 3 / 6 / 12 ms | 334 | 4.3 | none |
| Kafka | 300.4k (100.1 %) | 99.9 % | 12 | 0.07 ms | 2.2 / 275 / 381 ms | 3 / 274 / 378 ms | 314 | 2.8 | client window | |
| 400k | KubeMQ | 400.3k (100.1 %) | 99.9 % | -18 | 0.07 ms | 2.2 / 5 / 9 ms | 4 / 9 / 19 ms | 446 | 4.8 | none |
| Kafka | 400.2k (100.0 %) | 99.6 % | -63 | 838 ms | 19.9 / 631 / 945 ms | 20 / 625 / 936 ms | 419 | 3.1 | client window; publish-delay p99 838 ms | |
| 500k | KubeMQ | 500.5k (100.1 %) | 100.0 % | 293 | 0.07 ms | 2.4 / 7 / 11 ms | 4 / 15 / 28 ms | 552 | 5.0 | none |
| Kafka | not run: this side's ladder had ended | |||||||||
| 600k | KubeMQ | 600.4k (100.1 %) | 99.9 % | 149 | 0.06 ms | 2.8 / 10 / 18 ms | 5 / 20 / 34 ms | 659 | 5.2 | none |
| Kafka | not run: this side's ladder had ended | |||||||||
| 700k | KubeMQ | 700.4k (100.1 %) | 99.9 % | -161 | 0.06 ms | 3.7 / 16 / 27 ms | 6 / 24 / 37 ms | 760 | 5.5 | disk |
| Kafka | not run: this side's ladder had ended | |||||||||
| 800k | KubeMQ | 745.0k (93.1 %) | 93.1 % | -17,712 | 30,242 ms | 160.7 / 452 / 611 ms | 209 / 550 / 730 ms | 776 | 3.7 | disk; 93.1 % of target; producers 30.2 s behind schedule |
| Kafka | not run: this side's ladder had ended | |||||||||
| Target | Broker | Achieved | Last 60 s | Backlog Δ | Pub-delay p99 | Publish p50 / p99 / p99.9 | E2E p50 / p99 / p99.9 | Disk MB/s | CPU cores | Limit seen |
|---|---|---|---|---|---|---|---|---|---|---|
| 300k | KubeMQ | not run: this side's ladder had ended | ||||||||
| Kafka | 300.1k (100.0 %) | 100.1 % | 796 | 0.07 ms | 2.7 / 383 / 717 ms | 3 / 387 / 717 ms | 314 | 2.7 | client window | |
| 400k | KubeMQ | not run: this side's ladder had ended | ||||||||
| Kafka | 400.5k (100.1 %) | 100.0 % | -584 | 0.07 ms | 88.3 / 1279 / 1599 ms | 92 / 1278 / 1599 ms | 416 | 2.7 | client window | |
| 500k | KubeMQ | not run: this side's ladder had ended | ||||||||
| Kafka | 481.0k (96.2 %) | 93.6 % | 462 | 11,610 ms | 965.7 / 2155 / 2520 ms | 966 / 2154 / 2521 ms | 498 | 2.8 | client window; 96.2 % of target; producers 11.6 s behind schedule | |
| 700k | KubeMQ | 700.4k (100.1 %) | 99.9 % | -3,333 | 0.06 ms | 4.1 / 20 / 44 ms | 9 / 63 / 102 ms | 758 | 4.8 | disk |
| Kafka | not run: this side's ladder had ended | |||||||||
| 800k | KubeMQ | 747.5k (93.4 %) | 93.3 % | 1,158 | 28,385 ms | 674.6 / 1311 / 1862 ms | 750 / 1414 / 1991 ms | 780 | 3.7 | disk; 93.4 % of target; producers 28.4 s behind schedule |
| Kafka | not run: this side's ladder had ended | |||||||||
Why is Kafka's tail long at light load?
results/2026-10-04/notes/kafka-light-load.md.100 KB messages
Bandwidth-bound workload with the 512 KB client (a 100 KB record cannot share a 128 KB batch). Disk sequential write on this rig is 820 MB/s per broker (fio). Both sides ran every rung; rungs past the point where a side stopped reaching its target are marked "extra".
| Target | Payload MB/s | Broker | Achieved | Last 60 s | Backlog Δ | Pub-delay p99 | Publish p50 / p99 / p99.9 | E2E p50 / p99 / p99.9 | Disk MB/s | CPU cores | Limit seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4,000 | 410 | KubeMQ | 4.0k (100.1 %) | 100.0 % | -21 | 0.10 ms | 2.3 / 5 / 9 ms | 4 / 9 / 13 ms | 432 | 4.0 | none | |
| Kafka | 4.0k (100.2 %) | 100.1 % | 12 | 0.10 ms | 108.3 / 1231 / 1548 ms | 107 / 1228 / 1545 ms | 411 | 2.4 | client window | |||
| 6,000 | 614 | KubeMQ | 6.0k (100.1 %) | 100.0 % | 1 | 0.10 ms | 3.2 / 10 / 29 ms | 6 / 17 / 52 ms | 643 | 4.5 | none | |
| Kafka | 4.7k (78.2 %) | 79.5 % | -2 | 60,000 ms | 1061.5 / 2266 / 2732 ms | 1059 / 2262 / 2727 ms | 479 | 2.5 | client window; 78.2 % of target; producers 60.0 s behind schedule | |||
| 8,000 | 819 | KubeMQ | 7.6k (95.0 %) | 95.1 % | -10 | 21,234 ms | 671.7 / 1438 / 2063 ms | 752 / 1542 / 2158 ms | 785 | 3.6 | disk; 95.0 % of target; producers 21.2 s behind schedule | |
| Kafka extra | 4.7k (59.2 %) | 59.0 % | 0 | 60,000 ms | 1057.2 / 2277 / 2688 ms | 1055 / 2272 / 2684 ms | 489 | 2.7 | client window; 59.2 % of target; producers 60.0 s behind schedule | |||
| 10k | KubeMQ | not run: this side's ladder had ended | ||||||||||
| Kafka extra | 4.7k (46.7 %) | 46.2 % | -5 | 60,000 ms | 1062.0 / 2391 / 2947 ms | 1060 / 2387 / 2948 ms | 483 | 2.5 | client window; 46.7 % of target; producers 60.0 s behind schedule | |||
Leader kill at 50k msg/s
At minute 5 of a 15-minute run the server leading the most partitions receives SIGKILL from the node and restarts on the same volume. A separate verifier publishes with an acked-ID ledger; every acked message must be read back. One kill per side.
| KubeMQ | Kafka | |
|---|---|---|
| Acked messages lost (verifier verdict) | 0 of 3,891,500 (PASS) | 0 of 3,786,100 (PASS) |
| Longest interval with zero acks | 2 s | 8 s |
| Killed server Ready | 13 s | 23 s |
| All replicas in sync | 31 s | 59 s |
| Victim was also controller / control-shard leader | [recorded in kill/] | no — derived: restarted node rejoined as KRaft follower, epoch 1 unchanged, leader node 2 (kill/boot/bench-combined-0.log.txt 08:58:02) |
| Locator tie-break used | no | no |
| Kill proof (restart count +1, same volume inode, same image digest, survivors untouched) | PASS | PASS |
| Timeouts left at default | Raft election (KubeMQ defaults) | broker.session.timeout.ms 9 s |
Resources
From 1-second Prometheus samples on the broker nodes during the measured window, per rung both sides ran, 128 KB client. The highlighted row is the highest rung both sides reached. "Cores per 100k" = sum of the three brokers' CPU ÷ carried rate. Memory is the container working set as the kernel accounts it, which includes page cache. Bold follows the tie rule.
| Rung | Broker | CPU cores (of 7) | Working set | Page cache (node) | Disk write MB/s | Disk busy | Bytes written / payload byte | NIC MB/s | Cores per 100k (cluster) | $ per billion msgs |
|---|---|---|---|---|---|---|---|---|---|---|
| 100k | KubeMQ | 3.8 | 3.2 GiB | 27 GiB | 117 | 15 % | 1.14 | 236 | 11.5 | $3.2 |
| Kafka | 2.0 | 15.5 GiB | 24 GiB | 104 | 12 % | 1.02 | 216 | 5.9 | $3.2 | |
| 200k | Kafka extra | 2.4 | 17.4 GiB | 24 GiB | 206 | 25 % | 1.00 | 422 | 3.6 | $1.6 |
| 300k | KubeMQ | 4.3 | 2.5 GiB | 27 GiB | 334 | 35 % | 1.09 | 649 | 4.3 | $1.1 |
| Kafka | 2.8 | 24.6 GiB | 24 GiB | 314 | 38 % | 1.02 | 635 | 2.8 | $1.1 | |
| 400k | KubeMQ | 4.8 | 2.9 GiB | 27 GiB | 446 | 45 % | 1.09 | 860 | 3.6 | $0.8 |
| Kafka (target not reached) | 3.1 | 24.8 GiB | 24 GiB | 419 | 51 % | 1.02 | 841 | 2.3 | — | |
| 500k | KubeMQ | 5.0 | 3.4 GiB | 27 GiB | 552 | 52 % | 1.08 | 1,064 | 3.0 | $0.6 |
| 600k | KubeMQ | 5.2 | 3.9 GiB | 27 GiB | 659 | 59 % | 1.07 | 1,266 | 2.6 | $0.5 |
| 700k | KubeMQ | 5.5 | 4.2 GiB | 27 GiB | 760 | 64 % | 1.06 | 1,465 | 2.3 | $0.5 |
| 800k | KubeMQ (target not reached) | 3.7 | 5.1 GiB | 26 GiB | 776 | 86 % | 1.02 | 1,550 | 1.5 | — |
$ per billion messages = hourly on-demand list price of that cluster's 3 broker nodes ($1.17/h) ÷ messages carried per hour, × 10⁹.
Rig and versions
| Brokers | 3 × n2-standard-8 (Intel(R) Xeon(R) CPU @ 2.60GHz), 7 CPU / 26 GiB per broker, one per node |
| Disk | 2 × 375 GB local NVMe as RAID-0; fio sequential write 820 / 820 MB/s (Kafka / KubeMQ cluster), synced writes 12,071 / 12,193 per s |
| Network | iperf3 between brokers: 15.5 / 15.5 Gbit/s |
| Driver | 2 × n2-standard-8, OpenMessaging Benchmark (commit in omb/VERSION), stock Kafka driver, identical YAML both sides except bootstrap; 128 KB and 512 KB batch profiles; consumer socket receive buffer: KubeMQ runs receive.buffer.bytes=-1, Kafka runs client default (64 KB) |
| Kafka | 4.3.1, KRaft combined broker+controller (non-recommended for production, chosen for symmetry with KubeMQ's in-process Raft), Strimzi 1.2.0, image quay.io/strimzi/kafka@sha256:e90a1a74af4226f3ca4d1ebef3ab13bdb09754ae17ca4c1444f7fcbb0ca8ea9a, heap 6 GiB, tuned config in infra/kafka/kafka.yaml, RF 3, min ISR 2 |
| KubeMQ | v3.6.22, image europe-docker.pkg.dev/kubemq/images/kubemq-next:v3.6.22@sha256:87f3e1aa3cf3e4c324893e482d8ee4e20f53e40390b41e8722b472854eb801be, chart 3.5.0, shipped defaults (8 shards, leaders balanced, append-only log, default ack policy), GOMEMLIMIT 20 GiB |
| What an ack means | Kafka: acks=all, the leader waits for every in-sync replica (normally 3 of 3) to have the write in the OS; no per-write fsync. KubeMQ: ack when 2 of 3 Raft members have the write in the OS; fsync in the background. These are not identical. |
Method
Consumers run with receive.buffer.bytes=-1, so the kernel sizes the socket receive buffer; the kafka-clients default pins it at 64 KB, which at these rates adds a stall of about 250 ms per consumer connection a few times a minute, a property of the client and not of either broker. The Kafka runs predate this setting and used the default; their end-to-end latency tracks their publish latency at every rung, so it is set by the producer side, not by the consumer buffer. Each KubeMQ run started on a freshly reinstalled KubeMQ with empty volumes; each Kafka run started on the running cluster after the previous run's topic was deleted. Fixed-rate runs only (OMB's unthrottled mode understates one side). The ladder climbed in 100k steps until a side achieved less than 99 % of target or its producers fell behind schedule; later rungs are marked "extra". No pass/fail verdict is applied; latency and resource figures are reported as measured at every rung. One run per cell; a run with an infrastructure fault is VOID and rerun once (one Kafka kill attempt was void for a harness fault and rerun). One discarded warm-up after every deploy; topic deleted and disk hygiene checked between runs. Kafka: tuned (tuning/kafka.md); KubeMQ: shipped defaults. Not tested: fan-out, soak, backlog drain, many partitions, strict fsync, repeats. Full method: docs/METHOD.md.
Reproduce
echo "$KEY" > .license.txt terraform -chdir=infra/terraform init scripts/build-images.sh scripts/up.sh kafka ; sleep 120 ; scripts/up.sh kubemq caffeinate -i scripts/matrix.sh --slot a # terminal 1 caffeinate -i scripts/matrix.sh --slot b # terminal 2 scripts/report.py results/2026-10-16 results/2026-10-04 scripts/down.sh --slot a ; scripts/down.sh --slot b
Raw OMB JSON, 1 s Prometheus exports, verifier ledgers, fio and iperf baselines, image digests: results/2026-10-16/, results/2026-10-04/.
Generated by scripts/report.py from results/2026-10-16 on 2026-10-18 07:34 UTC.
Run it on your own numbers
Every script, workload and raw result is in the repository. Bring up the same two clusters, run the matrix, and compare against your own traffic shape.