# 🚀 Throughput, latency and backpressure compared

## Learning objectives

- Explain how batching, compression and sequential disk writes give Kafka its throughput ceiling.
- Explain how prefetch and per-message acknowledgements shape RabbitMQ's latency floor and in-flight limit.
- Size prefetch and consumer concurrency with Little's Law instead of guessing.
- Predict which broker saturates first for a given volume and latency target, and say why.

## Quick review

Last lesson: ordering is a property of a container, not a broker. A Kafka partition orders; a RabbitMQ queue with one consumer orders. The key decides which container an event lands in.

Today we reuse that idea from the other side. **The number of containers is also your parallelism budget.** More partitions means more parallel batches. More queues means more parallel consumers. That single sentence explains most of the throughput difference you will measure.

## Theory

### Kafka: throughput comes from amortization

Kafka writes to an append-only log. Writes are sequential, and sequential I/O on a single spinning disk beats random I/O by orders of magnitude. Reads come from the page cache and go out through zero-copy `sendfile`. Neither path needs a per-message index lookup, which is why read cost stays flat as the log grows to terabytes.

The producer then amortizes everything else over a batch:

- `batch.size` is the byte ceiling per partition.
- `linger.ms` is the time ceiling. A record waits up to this long for company.
- Compression (lz4, zstd) is applied per batch, so bigger batches compress better.

The trade is explicit: `linger.ms` is latency you pay on every single record. On the consumer side, `fetch.min.bytes` and `fetch.max.wait.ms` do the same job in reverse.

### RabbitMQ: latency comes from push and small units of work

RabbitMQ pushes a message to a consumer, waits for an ack, and repeats. Every message is routed, delivered, tracked and acknowledged individually. That per-message bookkeeping is expensive at volume, but it is also why a single message can cross the broker in well under a millisecond when the queue is empty.

You cannot batch your way out of this. The lever is `basic.qos` prefetch: how many unacknowledged messages a consumer may hold. Prefetch 1 gives strict serialization and the smallest redelivery window. Prefetch 250 parks 250 messages in one consumer's memory and, if that consumer dies, redelivers all 250. Quorum queues add a Raft majority round trip to every publish and ack.

| Dimension | Kafka | RabbitMQ |
| --- | --- | --- |
| Unit of work | batch of records in a partition | one message per delivery and ack |
| Throughput lever | partitions, batch size, compression | queues, consumers, prefetch |
| Latency floor | set by `linger.ms` (single-digit ms) | sub-millisecond to a few ms when shallow |
| Slow consumer shows as | consumer lag | queue depth and unacked count |
| Buffer limit | retention time and size | memory and disk alarms |

### Backpressure: pull versus push

This is the part that surprises people in production.

RabbitMQ applies backpressure explicitly. A slow consumer grows the queue, queue depth eats memory, the memory high watermark (default 0.6 of RAM) or `disk_free_limit` trips, and the broker blocks publishers. The client sees `connection.blocked`. Your producers slow down, whether you planned for it or not. It is crude but honest: nothing is silently lost.

Kafka consumers pull. A slow consumer cannot push back on the broker, because the broker never pushes. The consumer simply accumulates lag, and the log absorbs the difference until `retention.ms` or `retention.bytes` deletes it. So ==Kafka's backpressure signal is data expiring before it is read==, which is far quieter than a blocked connection. Producer-side backpressure is separate: `buffer.memory` fills and sends block for up to `max.block.ms`.

## Worked examples

### Example 1: sizing prefetch with Little's Law

In-flight messages equal throughput multiplied by latency: $L = \lambda \times W$.

You need 6,000 msg/s across 24 consumers, so 250 msg/s per consumer. p99 processing time is 40 ms.

$$
L = 250 \times 0.040 = 10
$$

Prefetch must be at least 10, and you need roughly 10 concurrent workers per consumer to feed them. Compare the naive setup: prefetch 250 with one thread gives $1/0.040 = 25$ msg/s no matter what. You would be 10x short while holding 250 unacked messages as a redelivery bomb.

### Example 2: what `linger.ms` actually buys you

Producer volume is 15,000 messages/s at 1 KB, so 15 MB/s. With 6 partitions, each partition sees 2.5 MB/s. `batch.size` is 1 MB, `linger.ms` is 5.

Filling 1 MB at 2.5 MB/s takes 400 ms, so the size ceiling never fires. The actual batch is $2.5 \text{ MB/s} \times 0.005\text{ s} = 12.5$ KB, about 12 records.

You are paying per-batch overhead 200 times a second. Raise `linger.ms` to 20 if your latency budget allows: batches grow to 50 KB, overhead drops 4x, and lz4 starts finding real redundancy. `batch.size` is a ceiling, `linger.ms` is the dial at moderate volume.

### Example 3: which one saturates first

Workload: 40,000 events/s at 2 KB, so 80 MB/s, with a p99 end-to-end target of 200 ms.

Kafka: spread over 12 partitions, each writes 6.7 MB/s sequentially, comfortable for one node. Latency is `linger.ms` plus one `acks=all` round trip plus fetch, so tens of milliseconds. Headroom is real, and if you lose it you add partitions and brokers.

RabbitMQ: 40,000 msg/s with per-message acks on quorum queues needs many queues and connections, and every publish pays a majority round trip. Any consumer stall grows the queue, raises memory, and triggers flow control that blocks your publishers. p99 degrades long before the mean does.

Verdict: sustained 80 MB/s with a 200 ms budget belongs on Kafka. Flip the numbers, 200 events/s with a 5 ms budget, and RabbitMQ wins easily because nothing waits for a batch.

> [!NOTE]
> These figures are order-of-magnitude reasoning, not benchmarks. Measure your own message size, replication factor and hardware before committing.

## Discussion & reflection prompts

1. Your RabbitMQ publishers got `connection.blocked` during a consumer deploy. What did the broker protect you from, and what did it hide?
2. Your Kafka consumer group is 6 hours behind and retention is 24 hours. What alarm would you rather have than lag, and why is it hard to build?
3. Where in your system would a 20 ms `linger.ms` be invisible, and where would it break an SLA?

## Expert corner

- Benchmark with realistic depth. An empty RabbitMQ queue answers in microseconds. Once depth grows into the millions, a single queue's Erlang process becomes the bottleneck, while Kafka's read cost does not move.
- Report p99, never the mean. Batching and backpressure both live in the tail.
- Set `linger.ms` to roughly a tenth of your latency budget. Below that you are buying throughput you already have; above it you are burning SLA.
- Treat prefetch as a memory and risk budget, not a speed dial. Throughput comes from concurrency and consumer count.
- `acks=all` and quorum queues both add a replication round trip. Budget for it in the latency target rather than discovering it in production.
- Watch two different signals: consumer lag for Kafka, queue depth plus unacked count for RabbitMQ. They fail in opposite directions.

## Summary & next steps

Kafka buys throughput by amortizing cost over batches and sequential writes, and pays for it in a latency floor set by `linger.ms`. RabbitMQ buys low latency with per-message push and ack, and pays for it in per-message overhead and a single-queue ceiling. Backpressure differs just as sharply: RabbitMQ throttles the producer, Kafka lets the consumer fall behind into the retention window.

Next up: routing and topology. You will see how exchanges and bindings compare with topics and partitions, and how routing complexity changes the throughput numbers we just built.

```flashcard
[
  { "front": "What gives Kafka its high throughput?", "back": "Sequential log appends, plus batching and compression that amortize cost over a whole batch instead of one record." },
  { "front": "Which two producer settings control Kafka batching?", "back": "batch.size (byte ceiling per partition) and linger.ms (time ceiling)." },
  { "front": "What does linger.ms cost you?", "back": "Latency: every record waits up to that long for a batch to fill." },
  { "front": "What controls in-flight messages in RabbitMQ?", "back": "Prefetch (basic.qos): the maximum unacknowledged messages a consumer may hold." },
  { "front": "High prefetch with a single-threaded consumer: what happens?", "back": "Throughput stays at 1/processing time, but many messages sit unacked and all get redelivered if the consumer dies." },
  { "front": "What is Kafka's backpressure signal on the consumer side?", "back": "Consumer lag. The log absorbs it silently until retention expires and the data is deleted." },
  { "front": "How does RabbitMQ push back on producers?", "back": "Memory high watermark or disk_free_limit trips flow control and blocks publishers (connection.blocked)." },
  { "front": "Little's Law as used for prefetch sizing?", "back": "In-flight = throughput x latency (L = lambda x W)." },
  { "front": "Why does Kafka read cost stay flat as the log grows?", "back": "Consumers fetch sequentially by offset; there is no per-message index scan or deletion." },
  { "front": "Which settings add a replication round trip to publish latency?", "back": "acks=all with min.insync.replicas in Kafka, and publisher confirms on RabbitMQ quorum queues." },
  { "front": "When does RabbitMQ beat Kafka on latency?", "back": "Low to moderate volume with small messages, when per-message delay dominates and you cannot afford a batching wait." },
  { "front": "What does fetch.max.wait.ms do?", "back": "Caps how long a Kafka consumer waits to reach fetch.min.bytes before returning a smaller batch." }
]
```

Cloze check: Kafka amortizes cost per {{c1::batch}} rather than per message. RabbitMQ's prefetch caps {{c2::in-flight unacknowledged messages}} per consumer. Kafka consumers are {{c3::pull}}-based, so a slow consumer shows up as {{c4::consumer lag}} instead of throttling producers. Little's Law gives in-flight = {{c5::throughput x latency}}.
