Skip to main content

Release performance against Kafka 4.3.1 — 2026-09-30

This is a local throughput, CPU, and memory experiment on one shared Linux host. All reported comparisons use the same Kafka 4.3.1 client, broker resource limits, payload, acknowledgments, replication contract, and number of repetitions. Medians summarize three independent topics; the accompanying CSV retains each run.

The tables are the archived benchmark results for the exact sources and binary hashes below. Publication rebased the changes onto broker a5142413 and protocol a6684fca, which had advanced while the benchmark was running. The broker pins protocol commit 8e385ae182b89dca6c6b119ab865142a6a57d0ea from krabka-protocol CRC follow-up. These archived numbers do not claim a rerun of the rebased PR revision.

Changes and evidence

The first CPU profile showed repeated first-batch CRC verification, decompression, and record allocation through should_roll_for_incoming. Cache the first record’s timestamp per segment, invalidate it on rollback/truncation, and recover it lazily on reopen. This preserves CreateTime/LogAppendTime behavior and negative timestamp deltas. The next profile confirmed that repeated owned batch decoding disappeared. It also identified request guards cloning/dropping every metrics handle; those guards now borrow the metrics for their existing lifetime.

The final random-payload/LZ4 CPU profile confirms the repeated roll-check decoding is gone. It attributes about 28% of sampled time to the sendfile path, 5% to the new CRC backend, and 5% cumulatively to record parsing. For zero-filled compressed traffic, decompression and record validation remain much larger costs. CPU sample shares are qualitative attribution, not instruction counts or a demonstrated universal throughput ceiling.

--config=release selects -c opt, thin LTO, and one codegen unit. Pilot runs of both source fixes with ordinary opt versus thin LTO had medians of 569,003 versus 643,790 records/s, and 17.894 versus 17.495 broker CPU seconds for 10 million 1 KiB random records. The small CPU difference is inconclusive; binary size fell from 80,446,224 to 57548696 bytes. These pilots are separate from the matched matrix below.

The workload accepts optional compression and zeros|random arguments. A seeded 4 MiB random pool prevents mostly zero payloads from masquerading as incompressible traffic. Every record retains its timestamp and unique sequence number. Existing seven-argument invocations retain their previous LZ4/zeros behavior.

Contiguous CRC follow-up

After the archived broker comparison, the borrowed batch validator and decoder were changed to hash the contiguous covered header and raw body in one call. They avoid a separate 40-byte header CRC and seeded digest setup, without copying or allocating a combined buffer. CRC coverage remains bounded by batch_length.

A release microbenchmark compared protocol 0536491e with 8e385ae1 on this host, using thin LTO, one codegen unit, CPU affinity to core 2, and five alternating before/after trials. Each timed public API call checks one uncompressed batch containing one record. Medians in nanoseconds per batch:

Value bytes Validate before Validate after Borrowed decode before Borrowed decode after
0 13.96 6.06 19.82 12.29
64 13.37 8.29 18.13 12.61
1,024 39.75 34.48 44.84 36.98

These are isolated API wall times, not whole-broker CPU or memory measurements. A direct CRC probe found negligible gains at 64 KiB and 1 MiB, where hashing dominates setup. The archived Kafka tables do not include this follow-up. Raw trials and probe sources are under crc-probe/ and record-probe/ in the local artifact directory below. The checked-in records benchmark now includes small validation cases, and a regression test verifies that a corrupt adjacent batch cannot affect the first batch’s CRC.

Contract and provenance

  • Baseline broker commit: 401945732d9ac37b93de5c89d5e649ece18049b0.
  • Kafka: apache/kafka:4.3.1, pinned as apache/kafka@sha256:77e3df9054047a88b520d0cc46e16696d3b22022e1d580aeccd2632df6532837.
  • Native runtime: the repository’s Wolfi production image, immutable local image sha256:66999791b48d10e3ef53a5c3ccaf0b7d30389040138e5476392540ad43b7662e, with the measured binary mounted read-only. Initial Alpine/glibc launch failures were archived and contributed no measurements.
  • Host: AMD EPYC 4344P, 8 physical/16 logical CPUs, about 61 GiB RAM, RAID-backed disk, loopback networking. No TLS, authentication, tiered storage, compaction, forced per-append fsync, restart, or failure injection in these runs.
  • Each broker/controller: two physical cores, 2 CPU quota, 2 GiB memory, no swap. Kafka heap: -Xms1g -Xmx1g, except the explicitly labeled smaller-heap check.
  • RF1: one combined broker/controller on cores 2–3; client on cores 0–1 and 4–7. RF3: three combined broker/controllers on cores 2–3, 4–5, and 6–7; client on cores 0–1 plus their SMT siblings 8–9.
  • Each topic: 12 partitions, RF1/minISR1 or RF3/minISR2, acks=all, idempotence, 65,536-byte producer batches, 5 ms linger. Clients use OpenJDK 25.0.4.1 and the exact Kafka 4.3.1 jars for both brokers. Admin JVMs run outside broker cgroups.
  • RF1 warm-up: 3 million records; RF3: 1 million records. Each measured topic is fresh, while the broker remains alive across the same ordered case sequence. RF1 samples: 10 million records for 1 KiB/100 B LZ4, 5 million uncompressed, 30,000 for 100 KiB, or 600,000 at 20k/s for 30 seconds.
  • CPU is the broker cgroup’s cpu.stat usage delta, including system CPU; RF3 adds all three brokers. CPU per record uses acknowledged records, not replicas. Monitoring includes client initialization/closing, whereas workload throughput and latency exclude that initialization. Short unlimited cases consequently have a larger fixed overhead and more variance.
  • Peak process RSS, anonymous cgroup memory, and working set are sampled every 250 ms. Working set is memory.current - inactive_file; RSS excludes most disk page cache. These are observed peaks, not hard bounds. Profiling runs are excluded from memory comparisons because pprof allocates additional buffers.
  • Every completed workload consumed exactly its acknowledged count with zero producer errors and zero duplicate sequences. This validates delivery in the experiment; it is not a crash-durability qualification.

Single broker, RF1

Workload Kafka records/s Original Krabka Release Krabka Kafka CPU µs/record Original Krabka Release Krabka
1 KiB random, LZ4 628,804 471,057 649,112 1.553 2.359 1.672
100 B random, LZ4 2,749,274 2,993,714 3,722,611 0.306 0.379 0.325
1 KiB zeros, LZ4 2,180,883 1,507,289 2,168,170 0.204 0.579 0.330
1 KiB random, no compression 558,709 547,849 540,143 1.343 2.198 1.995
100 KiB random, LZ4 5,552 4,629 6,834 116.330 221.598 142.004
1 KiB random, 20k records/s 19,998 19,997 19,998 4.533 8.217 6.289
Workload Kafka peak RSS MiB Krabka peak RSS MiB Kafka working set MiB Krabka working set MiB Kafka p99 ms Krabka p99 ms
1 KiB random, LZ4 677.8 63.6 742.8 137.3 325.1 313.7
100 B random, LZ4 1,152.5 76.1 1,217.9 149.3 76.0 4.5
1 KiB zeros, LZ4 1,188.5 120.7 1,260.3 191.5 559.9 629.7
1 KiB random, no compression 1,199.1 122.8 1,281.4 200.9 335.7 367.1
100 KiB random, LZ4 1,215.0 123.2 1,304.8 201.5 310.4 370.2
1 KiB random, 20k records/s 1,220.4 123.4 1,318.9 200.4 3.3 3.4

Local protocol optimization

The table above uses the benchmark baseline’s original protocol dependency. The following variant also changes the sibling krabka-protocol checkout: use the runtime-dispatched crc-fast CRC-32C backend, preserve finalized seeded CRC semantics, and inline four existing varint readers. CRC equality was checked against the previous independent implementation for arbitrary seeds, unaligned buffers, and chained chunks. A standalone 64 KiB CRC microbenchmark improved from 7.38 to 63.4 GiB/s on this AVX-512 host; whole-broker gains are much smaller. The compressed-record profile’s sampled varint share fell from about 29% to 13%; inlining changes attribution, so that percentage is not a whole-broker CPU gain.

The measured binary used a local two-repository variant. The protocol edits were in /home/matt/.codex/worktrees/782f/krabka-protocol, branch codex/crc32c-performance, based on 753a8e2a6795dad438b587910afa05b43bf63e95. The implementation is now published in protocol PR #48 and pinned by this broker PR. An ordinary release build includes both repositories’ optimizations. Replaying the exact historical measured binary uses the original source patches and both repository overrides listed in builds.json and below.

Workload Kafka records/s Original Krabka Release Krabka Kafka CPU µs/record Original Krabka Release Krabka
1 KiB random, LZ4 628,804 471,057 636,407 1.553 2.359 1.562
100 B random, LZ4 2,749,274 2,993,714 3,663,164 0.306 0.379 0.307
1 KiB zeros, LZ4 2,180,883 1,507,289 2,827,004 0.204 0.579 0.271
1 KiB random, no compression 558,709 547,849 662,199 1.343 2.198 1.446
100 KiB random, LZ4 5,552 4,629 6,615 116.330 221.598 132.840
1 KiB random, 20k records/s 19,998 19,997 19,997 4.533 8.217 5.932
Workload Kafka peak RSS MiB Krabka peak RSS MiB Kafka working set MiB Krabka working set MiB Kafka p99 ms Krabka p99 ms
1 KiB random, LZ4 677.8 64.9 742.8 137.7 325.1 315.8
100 B random, LZ4 1,152.5 74.6 1,217.9 146.9 76.0 59.3
1 KiB zeros, LZ4 1,188.5 112.5 1,260.3 185.4 559.9 312.6
1 KiB random, no compression 1,199.1 113.8 1,281.4 190.8 335.7 340.9
100 KiB random, LZ4 1,215.0 113.8 1,304.8 191.6 310.4 324.9
1 KiB random, 20k records/s 1,220.4 113.9 1,318.9 190.1 3.3 3.4

This variant improves original Krabka’s 1 KiB random throughput by about 35% and lowers its CPU per record by about 34%. Against Kafka, the 1 KiB random result is effectively tied within host variance. The other unlimited RF1 rows show about 19–33% greater throughput, but CPU is still about 33% higher for compressible records, 8% higher without compression, and 14% higher for large records. At 20k records/s it uses about 31% more CPU than Kafka. These are separate throughput and efficiency outcomes.

The random 100 B unlimited case is short; its headline rate is not evidence of a sustained broker saturation ceiling. See the longer concurrent-client check.

Interleaved large-record recheck

The CRC-only sequential matrix had a slow 100 KiB row. Three interleaved rounds with a fresh broker per variant, 5,000 warm-up records, and 60,000 measured 100 KiB random LZ4 records did not reproduce that regression:

Variant Records/s CPU µs/record Peak RSS MiB p99 ms
Broker-only release 6,775 131.435 59.6 343.4
CRC-only 6,733 122.477 61.4 345.2
CRC and inline readers 7,052 123.825 62.5 336.6

CPU µs/record is lower-is-better; throughput is higher-is-better. RSS depends on heap policy and previous workloads: Kafka’s committed heap grows during the case sequence. The first 1 KiB row has substantially lower Kafka RSS than later rows. Highly compressible MiB/s measures logical payload bytes, not wire/disk bandwidth.

Three brokers, RF3/minISR2

CPU and memory below are aggregate cluster values. Throughput and latency are from the same client workload, with all replicas in the ISR before the run.

Workload Kafka records/s Original Krabka Release Krabka Kafka CPU µs/record Original Krabka Release Krabka
1 KiB random, unlimited 287,992 313,473 342,679 6.555 10.335 7.293
1 KiB random, 20k records/s 19,997 19,997 19,997 16.519 21.250 17.073
Workload Kafka peak RSS MiB Krabka peak RSS MiB Kafka working set MiB Krabka working set MiB Kafka p99 ms Krabka p99 ms
1 KiB random, unlimited 2,857.7 154.7 3,041.0 322.2 800.4 553.9
1 KiB random, 20k records/s 2,889.0 165.4 3,136.0 362.1 5.3 4.0

The local protocol variant, with the same RF3 contract:

Workload Kafka records/s Original Krabka Release Krabka Kafka CPU µs/record Original Krabka Release Krabka
1 KiB random, unlimited 287,992 313,473 317,700 6.555 10.335 7.245
1 KiB random, 20k records/s 19,997 19,997 19,997 16.519 21.250 17.258
Workload Kafka peak RSS MiB Krabka peak RSS MiB Kafka working set MiB Krabka working set MiB Kafka p99 ms Krabka p99 ms
1 KiB random, unlimited 2,857.7 152.8 3,041.0 328.0 800.4 761.0
1 KiB random, 20k records/s 2,889.0 163.8 3,136.0 372.9 5.3 5.8

The broker-only release is about 19% faster than Kafka in this RF3 run, while using about 11% more aggregate CPU per record. The local protocol variant is about 10% faster, with similar CPU to the broker-only variant. This difference does not establish a protocol throughput improvement for replication.

Repeated measurements and follow-up checks

RF1 workload Kafka records/s range Broker-only range Local protocol range
1 KiB random, LZ4 501,852–641,363 634,823–663,519 598,709–657,292
100 B random, LZ4 2,630,459–2,779,463 2,590,848–3,757,029 3,418,418–3,705,671
1 KiB zeros, LZ4 2,118,178–2,324,273 1,990,538–2,198,121 2,824,116–2,870,377
1 KiB random, no compression 548,946–567,907 436,430–552,186 627,047–784,212
100 KiB random, LZ4 5,482–6,362 5,880–7,791 6,288–7,386
1 KiB random, 20k records/s 19,997–19,998 19,997–19,998 19,997–19,998

Linux TCP packet batching experiment

Three interleaved rounds compared the local protocol variant with and without TCP_CORK around file-backed fetch responses, using a fresh broker each time. The socket is uncorked on success and I/O errors; cancellation already requires the caller to close a partially written response. TLS fallback and other OS paths are unchanged. Results below determine whether this additional code stays.

Workload Before records/s Cork records/s Before CPU µs/record Cork CPU µs/record Before p99 ms Cork p99 ms
1 KiB random unlimited 616,753 672,844 1.587 1.565 320.908 301.634
1 KiB random 20k/s 19,997 19,997 6.060 5.954 3.397 3.389

The TCP_CORK experiment was removed: median CPU gains were only 1.4% unlimited and 1.8% at 20k/s, below the materiality threshold and host variance. Its 9% median throughput difference came with overlapping 406k–686k versus 606k–692k ranges and an isolated p99 spike. The simpler previously validated release-final binary is used for follow-up comparisons.

A final isolated record-validation check compared the existing parser’s inline hint with forced inlining. Three interleaved samples of 64 KiB batches, with 100 B/1 KiB zero values and no compression/LZ4, showed no material improvement (median changes within about 1%). The last round had substantial host variance. No parser logic or additional inline attribute was retained. The corrected experiment and compiler output are retained in record-probe/forced-results.txt; the earlier duplicate-attribute trial is not evidence of an optimization.

Four concurrent producer/consumer pairs

Four independent topics each have 12 partitions (48 total). Each client writes and reads 5 million 1 KiB random, 40 million 100 B random, or 10 million 1 KiB zero records. Rate uses the total count divided by full wall time including client startup and shutdown, unlike the single-client matrix. Latency values are the worst client quantile, not a merged distribution; each client JSON is retained. All four JVMs share the same six client cores.

Workload Kafka records/s Krabka records/s Kafka CPU µs/record Krabka CPU µs/record Kafka RSS MiB Krabka RSS MiB Kafka p99 ms Krabka p99 ms
1 KiB random 577,012 580,839 1.829 1.974 1203.0 118.6 417.478 522.597
100 B random 2,697,986 4,209,653 0.287 0.233 1231.3 144.5 840.101 566.763
1 KiB zeros 3,406,950 2,307,669 0.184 0.324 1230.9 188.4 2357.737 2714.179
Workload Kafka records/s range Krabka records/s range
1 KiB random 551,196–654,104 527,346–605,226
100 B random 2,642,047–5,031,914 3,821,041–4,286,916
1 KiB zeros 3,258,908–3,523,161 2,303,012–2,344,034

Fresh broker with Kafka 256 MiB heap

Both brokers start fresh, warm up with 3 million 1 KiB random records, then run three unlimited and three 20k/s topics. Kafka uses -Xms256m -Xmx256m; native broker settings remain the same. This check measures the effect of a smaller Kafka heap instead of attributing the entire memory gap to implementation.

Workload Kafka records/s Krabka records/s Kafka CPU µs/record Krabka CPU µs/record Kafka RSS MiB Krabka RSS MiB Kafka p99 ms Krabka p99 ms
1 KiB random unlimited 489,759 520,989 2.234 1.876 412.3 70.7 351.493 402.750
1 KiB random 20k/s 19,998 19,998 5.404 6.630 409.6 73.4 3.829 3.748
Workload Kafka records/s range Krabka records/s range
1 KiB random unlimited 455,308–501,800 502,913–591,391
1 KiB random 20k/s 19,997–19,998 19,997–19,998

Interpretation and limits

This host runs other workloads, and the measurements have visible variance. The small RF1 1 KiB throughput lead over Kafka should not be treated as a decisive win. Some single-client tests leave broker CPU below its limit; their throughput is an end-to-end workload rate, not a demonstrated broker saturation ceiling. CPU efficiency, throughput, and latency can move in different directions.

These results establish local behavior for these binaries and contracts. They are separate from the existing Kind performance qualification, partition-envelope results, remote CI, and release readiness.

The original native container inherited a 1,024-descriptor soft limit. Completed matrix runs stayed within it, but a later profiled random-record run after a 60-million-record profile exhausted descriptors and triggered the broker’s shutdown path. That incomplete run is excluded. All subsequent fresh-broker, packet-batching, concurrent, and small-heap runs explicitly use --ulimit nofile=131072:131072 for both brokers; saved /proc limits establish the setting. There was no OOM. This experiment does not qualify default-container behavior at an accumulated multi-topic file-descriptor limit.

Reproduction and retained artifacts

Build the current broker, including the published protocol pin, with:

bazel build --config=release --lockfile_mode=off --bes_backend= \
  //crates/broker:krabka-broker //crates/format:krabka-format
bazel run --config=release --lockfile_mode=off --bes_backend= //packaging:image_load

To replay the historical local variant on the archived broker source, add these to the build (the first directory preserves Bazel-generated BUILD files, and the second supplies its dependency metadata; a raw source checkout alone cannot replace those generated files):

--override_repository=rules_rs++crate+crates__github.com_krabka-io_krabka-protocol_753a8e2a=/home/matt/.cache/krabka-perf/782f-20260930/protocol-source-override
--override_repository=rules_rs++crate+crates__krabka-protocol-0.4.0=/home/matt/.cache/krabka-perf/782f-20260930/protocol-crate-override

Compile packaging/performance/BrokerPerformanceWorkload.java against Kafka 4.3.1 jars. Its complete argument contract is:

bootstrap topic group records bytes records_per_second timeout_seconds [compression] [zeros|random]

The exact local runner, container commands, configs, topic descriptions, resource time series, per-client JSON, profiles, build hashes, and validation logs are in /home/matt/.cache/krabka-perf/782f-20260930. run.py, matrix.py, rf3.py, and concurrent.py record the launch and workload commands. Paths in this experimental runner refer to this checkout; adapt ROOT/ART and the native runtime image ID for another host. builds.json records the source patch and binary SHA-256 hashes. Disposable broker record logs were removed after count/sequence validation to avoid disk exhaustion; configs, measurements, profiles, and binaries remain.

performance-kafka-4.3.1.csv contains each matched unprofiled run, including throughput, CPU seconds, peak RSS/anonymous/working-set memory, and p50/p95/p99. Profiles are retained in krabka-1k-profile, cache-1k-profile, and release-rf3-optimized-profile.

The following hashes identify the main native binaries independently of mutable Bazel outputs:

Binary SHA-256 Bytes
release-baseline 7768013ce895d53eb9931446aff6d75626c27af3648ec0dd02dda0096e645a4c 80,432,424
release-optimized dcde23b3a2435fb6f481906017db1df4ece2ab7118d28ddd8a4a0341617a1c0d 57,548,696
release-crc 708492a9c052aea7d6df9bccb4945f43be3fe40bb6034a2647b7eecbe4c4f0cb 57,599,688
release-final 6083b08058af05b7ed63c6bf637202b9fb1d996d2b32911bf7c2a841b43c5395 57,882,720

Validation

474 log unit tests, 4 restart tests, and all 4,620 broker unit tests passed with -c opt and the local protocol overrides after removing the packet-batching experiment. The experiment separately passed 4,621 broker unit tests. Broker and log Bazel Clippy targets passed. All 1,137 protocol unit tests and protocol Cargo Clippy (--all-targets --locked -D warnings) passed in the sibling checkout. Thin-LTO release builds completed the measured workloads. git diff --check and formatting passed. No remote CI conclusion is claimed.

Publication revalidation after the rebase and published protocol pin passed 4,634 broker unit tests, 474 log unit tests, and four restart tests through normal Bazel resolution, without repository overrides. Both repositories passed cargo clippy --workspace --all-targets --locked -- -D warnings; the rebased protocol companion passed 1,140 unit tests. Formatting and diff checks passed. The archived benchmark tables remain tied to their original binaries above.

The contiguous-CRC follow-up passed all 1,141 protocol unit tests and full workspace Clippy. The CI timeout repair leaves the Raft model and all state bounds intact: it gives only kraft_model_test a 900-second Bazel timeout, because its five cases exceeded the default 300-second limit on CI. All five cases passed locally with their pinned unique-state counts unchanged.

The same follow-up pin passed 4,634 broker unit tests, 474 log unit tests, four restart tests, full broker workspace Clippy, and formatting through normal dependency resolution. Updating newly yanked yoke-derive 0.8.3 to 0.8.4 passed all four cargo-deny checks.