Queues, task queues and commit logs
Point-to-point task queues (RabbitMQ, SQS) versus distributed append-only logs (Kafka, Pulsar).
What you will learn
- Use Little's law to see why slow synchronous work starves a thread pool.
- Take slow work off the request path with a queue and a quick acknowledgment.
- Compare task queues with a distributed commit log such as Kafka, and scale Kafka consumers by partition.
The synchronous bottleneck and thread pool starvation
In a traditional synchronous request-response architecture, an incoming HTTP request blocks an operating system or runtime worker thread for the entire duration of downstream processing:
If video transcoding or PDF rendering takes , a server with worker threads is fully saturated at just:
By Little's law (), 200 busy threads at 5 seconds each can sustain at most requests per second. Beyond that, requests queue up waiting for a thread, latency climbs, and clients (or proxies in front) start timing out.
Asynchronous decoupling: fast acknowledgment, deferred work
Asynchronous workflows take expensive operations off the request path. With a message queue:
- The application publishes a job to the queue and tells the user the job's status straight away (for example,
HTTP 202 Acceptedwith a status URL). The user is not blocked. - A worker picks up the job, processes it in the background at its own pace, and signals when it is done.
The client can even make the result look immediate: a posted tweet appears on your own timeline at once, though delivering it to all your followers takes longer. The cost: queues add delay and complexity, so cheap calculations and real-time workflows may be better done synchronously.
Common choices differ: Redis is a simple broker but can lose messages; RabbitMQ is popular but uses the AMQP protocol and you run the nodes; Amazon SQS is hosted but can have high latency and may deliver a message twice. Task queues such as Celery go further: they run the task and return its result, and can schedule jobs.
Task queues versus distributed commit logs
| Dimension | Message / task queue (RabbitMQ, SQS, Celery) | Distributed commit log (Kafka) |
|---|---|---|
| Storage | A buffer from which messages are removed once processed | An append-only log; events are not deleted when consumed |
| Consumption | Competing workers take messages; each message goes to one worker | Each consumer group tracks its own position (offset) in each partition |
| Ordering | Usually best effort, especially across retries (SQS FIFO queues are the exception) | Events with the same key go to the same partition and are read in exactly the order written |
| Replay | Not normally possible once a message is consumed | Rewind a consumer group's offset, for as long as the topic's retention keeps the events |
| Typical use | Background jobs: emails, transcoding, scheduled tasks | Event streams read by many consumers, change data capture, audit trails |
Kafka is built for throughput: it relies on sequential disk writes and the operating system's page cache, and uses sendfile to send data from the page cache to the network without copying it through the application.
Partitioning and consumer group scaling in Kafka
A Kafka topic is divided into partitions; by default an event's key is hashed to choose its partition:
- Ordering: Events with the same key (e.g.
user_id_492) land on the same partition, and every consumer reads a partition's events in the same order (as long as the number of partitions does not change). - Consumer groups: Each partition is consumed by exactly one consumer within a consumer group at any given time, so a group can use at most consumers in parallel (); extra consumers sit idle.
Task queue versus commit log
- Web & Mobile Clients: 1 × Clients & Users
- Anycast DNS / Route53: 2 × Anycast DNS / Route53
- Ingestion Load Balancer: 3 × Application Load Balancer
- Ingestion API Servers: 6 × App Compute Server
- Kafka / Event Stream: 3 × Message Queue / Kafka
- Transcode Worker Fleet: 8 × App Compute Server
- S3 Video Storage: 2 × Object Storage (S3)
- Web & Mobile Clients sends requests to Anycast DNS / Route53
- Anycast DNS / Route53 sends requests to Ingestion Load Balancer
- Ingestion Load Balancer sends requests to Ingestion API Servers
- Ingestion API Servers sends requests to Kafka / Event Stream
- Kafka / Event Stream sends requests to Transcode Worker Fleet
- Transcode Worker Fleet sends requests to S3 Video Storage
Key takeaways
- 200 threads holding requests for 5 seconds each serve at most 40 requests a second; past that, requests queue and time out.
- Publish a job, answer at once (for example 202 Accepted) and let workers do the work; queues add delay and complexity, so keep cheap work synchronous.
- A task queue removes each message once one worker processes it; a commit log keeps events so every consumer group reads at its own offset and can replay.
- Kafka keeps order per partition, and a consumer group can use at most one consumer per partition.
Sources
- System Design Primer: Asynchronism (message queues, task queues, back pressure)
- Apache Kafka documentation: Introduction (partitions, keys, retention)
- Apache Kafka documentation: Design (consumer groups, sendfile, delivery semantics)
Adapted from The System Design Primer by Donne Martin, licensed under CC BY 4.0. Changes were made.