Performance

Throughput and latency

One Felix broker accepts 4.4 GB/s with every message acknowledged, writes 1.4 GB/s fsynced to disk, and acknowledges a single fsynced publish in 0.31 ms — about a third of a millisecond. Three 4-core brokers keep three fsynced copies of every durable record at 656 MB/s. Every number here comes from the v0.6.0 performance write-up, which has the exact conditions and how to reproduce them.

The setup behind the single-broker numbers: four load generators in the same Azure region drive one broker on an 8 vCPU VM with local NVMe. In durable runs the broker fsyncs before it acks.same regionload gen 1load gen 2load gen 3load gen 4felix-broker8 vCPU Azure VMlocal NVMefsync before ack
Single broker

Measured on one broker

Felix 0.6.0 on an Azure Standard_L8as_v4 broker (8 vCPU, local NVMe), driven by 4 load generators in the same region over QUIC with TLS. "Durable" means the broker fsyncs before it acks. These numbers come from the final campaign build; the release build re-measured batched throughput within about 1% of them, and latency within about 5%, except small fire-and-forget messages (#1469). Each card links to the table it comes from.

0.6.0 · one broker · QUIC with TLS

Latency, one publish in flight

256 B messages, median of three runs. The solid bar is p50 and the tint runs to p99. Delivery is when a subscriber on the stream receives the message; a durable message is delivered only once it is durable.

In memory
ack0.16 / 0.20 ms
delivery0.16 / 0.20 ms
Durable, fsync every 250 ms (the default)
ack0.18 / 0.22 ms
delivery0.17 / 0.23 ms
Durable, fsync before every ack
ack0.31 / 0.39 ms
delivery0.31 / 0.39 ms

The fsync costs about 0.13 ms at 256 B. The tail stays tight: p999 is within 50% of p50 in every row. Unloaded latency has 4 KiB too.

Acked throughput by shape

Batches of 64 lift 256 B messages from 102 MB/s to 2.2 GB/s in memory, and durable 4 KiB and 64 KiB batches run at the disk's fsync rate. Throughput by shape has every cell.

Acked payload throughput on a log scale for in-memory and durable streams: batch 1 and 64 at 256 B and 4 KiB, batch 64 at 64 KiB, with the disk's fsync rate as a dotted line. Batch 64 lifts 256 B in-memory from 102 MB/s to 2.2 GB/s and durable from 53 to 482 MB/s; durable 4 KiB and 64 KiB batches sit on the disk line at 1.4 and 1.5 GB/s.
Latency

Fast when idle, predictable when saturated

With few publishes in flight, Felix acks in well under a millisecond. Push it to full throughput with thousands outstanding and acks wait in line for their turn: the wait is the work outstanding divided by the rate it completes, as Little's law predicts, and the measured p50s land on that prediction. Keep few requests in flight for sub-millisecond acks, or many for throughput. Two kinds of latency works through the numbers.

Ack latency against publishes outstanding

With one publish outstanding, latency is service time: 0.16 to 0.38 ms. Every point to the right is queueing, and sits close to the cross that Little's law predicts from throughput alone.

Ack latency p50 against the number of publishes outstanding, log-log, for in-memory and durable streams at 256 B and 4 KiB. Every line rises from 0.16 to 0.38 ms with one publish outstanding to 10 to 49 ms with 4,096, and each loaded point sits close to Little's law's prediction.
Comparison

Against NATS JetStream

NATS ran tuned, on the same VMs, with the same 64 publishers and payloads, interleaved with Felix. With both fsyncing every write before the ack, Felix is 1.8 to 3.5 times faster for single messages and for 4 KiB batches, and 14.8 times for batches of 256 B messages. With periodic fsync on both sides it is 1.25 times faster at 4 KiB and 9.5 times at 256 B, and 1.2 times in memory. Core NATS is 1.34 to 1.39 times faster at fire-and-forget, where it discards messages nobody is subscribed to.

The two systems are built differently, so no pair is an exact match. Read each ratio with where it still differs. The 14.8× durable batch pair is partly the benchmark tool: nats bench sends one atomic batch at a time, while Felix keeps 64 in flight. Full results are in Felix against NATS JetStream.

Felix rate divided by NATS rate

Ten pairs, payload at the server, log scale centred on 1×. Each bar names the pair's main caveat.

Felix's payload rate divided by NATS JetStream's for ten pairs on a log scale centred on 1×. Felix is faster in every acked and durable pair, from 1.19× to 14.8×; core NATS is faster for fire-and-forget, by 1.34× and 1.39×. Each bar names the pair's main caveat.
Cluster

Three brokers, three copies

On a durable Quorum stream with a replication factor of 3, every record is written to all three brokers and each broker fsyncs its copy. The publisher gets its ack once the shard's leader and at least one follower have fsynced, so an acknowledged record is already on two disks, and the third copy follows. Each stream's shard leaders are spread evenly over the brokers, so every broker carries its share of the writes. Each card links to the table it comes from.

Felix 0.6.0 release build on three Azure Standard_L4as_v4 brokers (4 vCPU, local NVMe), one in each availability zone, driven by 4 load generators over QUIC with TLS.

0.6.0 · three brokers · replication factor 3

Straight to the owner against through any broker

Payload with batches of 64, 64 in flight per publisher. A forwarded publish is decrypted and re-encrypted at every hop, and a durable one holds its shard until the quorum commits. ClusterClient avoids both. Forwarding has the numbers.

Payload with batch 64, straight to each shard's owner against through any broker: in-memory 256 B 2.0 GB/s against 864 MB/s, in-memory 4 KiB 4.6 against 2.9 GB/s, durable 256 B 475 against 107 MB/s, durable 4 KiB 656 against 512 MB/s.