Queues and event streams
Not every piece of work has to happen while the user waits. Put it on a queue and let another service do it when it can: the user gets a fast answer, a spike becomes a backlog instead of an outage, and services stop depending on each other being up at the same moment. This module covers the three shapes of messaging, what "delivered" really means, ordering, and how to handle messages that keep failing.
1. The idea
A producer sends messages; a broker stores them; consumers receive and process them. Messaging buys three things:
- Decoupling in time: the producer doesn't wait for, or even need, the consumer to be running.
- Absorbing bursts: 10× traffic for five minutes becomes a backlog that drains, instead of overloading the consumer.
- Fan-out: one event ("order placed") can feed many independent consumers (email, analytics, warehouse) without the producer knowing about them.
Three shapes of messaging
| Shape | How it behaves | Use for | Examples |
|---|---|---|---|
| Message queue | Each message is handled by one consumer, then deleted. Workers compete for messages. | Background jobs: send this email, resize this image, charge this card | Amazon SQS, RabbitMQ, Google Cloud Tasks, Redis Streams or BullMQ |
| Event log (stream) | Messages are appended to an ordered, durable log and kept (days, or forever). Each consumer group reads at its own position and can replay. | Event-driven systems, change data capture, analytics pipelines, many readers of the same events | Apache Kafka, Amazon Kinesis, Redpanda, Apache Pulsar |
| Pub/sub | A message published to a topic is pushed to every current subscriber. | Broadcasting: notifications, cache invalidation, real-time updates | Google Cloud Pub/Sub, Amazon SNS, Redis Pub/Sub (fire-and-forget) |
2. How it works
What "delivered" means
Networks and consumers fail mid-message, so every system picks a delivery guarantee:
- At-most-once: the message is removed before it's processed. A crash loses it. Acceptable for metrics you can afford to drop.
- At-least-once: the message is removed only after the consumer acknowledges it. A crash after processing but before the ack means it's delivered again. The usual default.
- Exactly-once delivery over a network isn't possible in general. What systems offer is effectively once: at-least-once delivery plus idempotent processing (a duplicate has no extra effect), or transactions that commit the result and the consumer's position together (Kafka's exactly-once within Kafka).
In practice: assume duplicates, and make consumers idempotent. Record processed message IDs, use upserts, or use the idempotency keys from Module 07.
Acknowledgements and the visibility timeout
In SQS, receiving a message hides it from other consumers for a visibility timeout (30 seconds by default). If the consumer deletes it in time, it's done; if the consumer crashes, the message reappears and another worker takes it. Set the timeout longer than the slowest normal processing time, or a slow worker and a second worker will process the same message at once.
Partitions, ordering and consumer groups
Kafka splits a topic into partitions. Messages with the same key go to the same partition and stay in order; there's no order across partitions. In a consumer group, each partition is read by exactly one consumer, so the number of partitions caps how many consumers can work in parallel. Each group tracks its offset (position) per partition. Press Play.
Messages that keep failing
A malformed message (a poison message) fails every time and, with naive retries, blocks or burns workers forever. After a few attempts (3–5 is typical), move it to a dead-letter queue (DLQ), alert on it, fix the bug, and replay it. Retries themselves should back off with jitter (Module 24), possibly through separate retry topics with increasing delays.
Back-pressure: when the queue keeps growing
A queue only absorbs bursts. If messages arrive faster than consumers process them on average, the backlog grows without limit, and latency with it. Watch queue depth and consumer lag; scale consumers on them, not on CPU; and if you can't keep up, push back on producers (reject, slow down) or shed low-value work. Try it in section 3.
Scaling writes: the strategies in order
The core idea is to reduce the write throughput each component handles. Do the math first, then go in order:
- Vertical scaling and the right database. Check you're actually hitting disk, CPU or network limits; modern machines are huge. For append-heavy data, a write-optimised store (Cassandra, a time-series database) trades read speed for write speed, because it appends instead of updating pages (Module 12).
- Sharding and vertical partitioning (Module 15): ten shards ≈ ten times the writes, if the key spreads load; split hot counters from content.
- Queues for bursts, load shedding for overload. A queue absorbs a spike and lets the database drain it steadily, but if input stays above capacity on average, the queue grows forever. Load shedding drops the least valuable writes when overloaded: a location ping (a fresher one arrives in seconds), impressions before clicks.
- Batching and hierarchical aggregation. Combine many small writes into a few: a "like batcher" sums likes per post per second, turning 100 writes into 1. For live events with millions of viewers, aggregate in layers (below).
| Prompt | Write-scaling plan |
|---|---|
| Instagram / social | Shard by user ID; separate profiles, posts and analytics; tiered storage for old posts |
| News feed | Balance celebrity fan-out on write against building feeds on read |
| Search indexing | Partition the index and batch the indexing pipeline |
| Live comments and likes | Hierarchical aggregation and batching |
State the trade-offs: queues add delay and eventual consistency; partitioning can make some reads harder; batching adds latency and moving parts, and loses the batch on a crash unless the source (a Kafka topic) can be re-read.
3. Try it yourself
Messages arrive at a steady rate with a burst in the middle. Workers each handle a fixed number per second. Find the worker count that drains the burst within your target, and see what happens when the average rate exceeds capacity.
4. Trade-offs
Async means "not done yet"
The user gets "accepted", not "finished". The product must show status ("processing…"), and you need a way to report failure later (Module 23 covers job status).
Queue or log?
A queue is simplest for jobs: built-in retries, visibility timeouts and DLQs. A log is better when many consumers need the same events, order per key matters, or you want to replay history.
Ordering costs parallelism
Strict order holds only within one partition (or one FIFO queue group). Order everything and you have one lane. Order only what must be ordered: per user, per account, per order.
Don't queue what should be fast and simple
Putting a 5 ms call behind a queue adds latency, moving parts and failure modes. Queue work that's slow, bursty, optional or must survive failures.
5. In real systems
- Amazon SQS: fully managed. Messages up to 1 MiB (larger payloads go to S3, with a pointer in the message), retained for 4 days by default and up to 14 days, a 30-second default visibility timeout, DLQs via a redrive policy. Standard queues give near-unlimited throughput with at-least-once delivery and best-effort order; FIFO queues give order per message group and deduplication.
- Apache Kafka: the most widely used event log. Since Kafka 4.0 (March 2025) it runs without ZooKeeper, using its own Raft-based KRaft controllers. Kafka 4.2 (February 2026) added share groups (KIP-932, "Queues for Kafka"), letting several consumers share one partition with per-message acknowledgement, after early access in 4.0. The current release in October 2026 is 4.3.
- Kafka durability: replicate partitions (replication factor 3), produce with
acks=alland a minimum in-sync replica count, and use idempotent producers to avoid duplicates on retry. - RabbitMQ offers flexible routing (exchanges), quorum queues for durability, and streams for log-style use. Google Cloud Pub/Sub and Amazon SNS are managed pub/sub; SNS commonly fans out into several SQS queues. Redis Streams suit modest queues where Redis is already running.
- Keep messages small: pass IDs and pointers ("video 123 uploaded to s3://…"), not the video. Brokers are built for many small messages.
The technologies up close
at-least-once, so consumers are idempotent), the key and ordering (
partitioned by order ID), and the failure path (
retries with backoff, then a DLQ we alert on). Don't use a queue to hide a database that can't handle normal load.
Apache Kafka
Situational · the event log at scaleA distributed, durable, append-only log that works as a message queue or an event stream. Lead with your partition key and your plan for hot partitions.
How it works
| Term | Meaning |
|---|---|
| Broker | A server in the cluster; more brokers = more storage and throughput |
| Topic / partition | A topic is split into partitions: ordered, immutable logs, the unit of parallelism and ordering |
| Key | hash(key) % partitions: the same key always lands in the same partition, in order. No key = spread evenly, unordered |
| Consumer group | Each partition is read by one consumer in the group; consumers pull at their own pace |
| Offset | A consumer's position in a partition; committed so it can resume after a crash |
- Queue vs stream: one group processing each message = a queue; many groups reading the same events independently, with replay = a stream. Retention defaults to 7 days.
- Durability: replication factor 3,
acks=all, a minimum in-sync replica count; the controller promotes an in-sync follower if a partition's leader dies. - Delivery: at-least-once by default (a crash before committing the offset means reprocessing); idempotent producers stop duplicates on retry; exactly-once within Kafka uses transactions.
- Failures: a dead consumer's partitions are rebalanced to the others; commit offsets only after the work is safely stored. Consumer retries aren't built in: use a retry topic, then a DLQ.
Status today (checked October 2026)
Kafka 4.3 is current (October 2026). Since 4.0 it runs without ZooKeeper (KRaft), and 4.2 added share groups for queue-style consumption. Managed: Confluent Cloud, Amazon MSK; Redpanda is a compatible alternative. Rough sizing: one broker handles on the order of a terabyte and up to about a million small messages a second, so below that, scaling Kafka isn't worth discussing.
Use it when
- Async work at scale with ordering per key (all events of one order in sequence)
- Many independent consumers of the same events, with replay (analytics, search indexing, CDC)
- Absorbing bursts in front of a slower system
Skip it when
- Simple job queues with built-in retries and DLQs (SQS is simpler)
- Large payloads: keep messages under 1 MB and send pointers to S3
- Request/response RPC
In the interview
Clicks go to a Kafka topic partitioned by
ad_id, replication factor 3 with acks=all. Consumers are idempotent because delivery is at-least-once; a viral ad gets a salted key so one partition doesn't melt.
- No partition key, or a skewed one: a viral ad sends everything to one partition. Fixes: salting (
ad_id + random, re-aggregate later), a compound key (ad_id + region), back-pressure. - Blobs in Kafka. Store the video in S3, send a pointer.
- Assuming exactly-once end to end without idempotent consumers.
- Spending time on "what if Kafka goes down?" A replicated cluster going down entirely is unrealistic; it's fair to say so politely and focus on consumer failures.
Amazon SQS (and simple job queues)
The default for background jobsA fully managed message queue: send, receive, delete, with retries and dead-letter queues built in.
How it works
A received message becomes invisible for a visibility timeout (30 s by default); delete it when done, or it reappears for another worker. After a set number of receives it moves to a dead-letter queue. Standard queues: near-unlimited throughput, at-least-once, best-effort order. FIFO queues: order per message group, deduplication.
Status today
Mature, serverless, pay per message; messages up to 1 MiB. Google Cloud Tasks and Azure Queue Storage are equivalents.
Use it when
- Background jobs: send this email, resize this image
- When you want retries and DLQs without building them
Skip it when
- Many consumers reading the same events, or replay (Kafka)
- Strict global ordering
In the interview
Jobs go on SQS with a redrive policy: three failed attempts and the message lands in a DLQ we alert on.
- A visibility timeout shorter than the job: two workers process the same message.
RabbitMQ
Situational · rich routingA self-hosted message broker with flexible routing through exchanges (direct, topic, fan-out), plus quorum queues and streams.
How it works
Producers publish to an exchange; bindings route each message to one or more queues; consumers acknowledge each message. Quorum queues replicate with Raft for durability.
Status today
Mature and widely deployed on-premises and as a managed service (Amazon MQ, CloudAMQP).
Use it when
- Complex routing rules between many services
- Teams already operating it
Skip it when
- Replayable event history at huge scale (Kafka)
In the interview
Fine to name if you know it; otherwise a queue such as SQS or RabbitMQ
is enough.
- Debating Kafka vs RabbitMQ at length. Interviewers care about the guarantee, the key and the failure path, not the brand.
Redis Streams and BullMQ
Situational · modest queues, Redis already thereQueues built on Redis: Streams with consumer groups and acknowledgements, or job libraries like BullMQ with delays, retries and priorities.
How it works
XADD appends; consumer groups read with XREADGROUP and acknowledge; a dead worker's pending items are reclaimed with XAUTOCLAIM. Durability follows Redis's persistence settings.
Status today
Popular for small and medium workloads where Redis is already running.
Use it when
- Modest job queues and notification fan-out alongside an existing Redis
Skip it when
- Long retention, replay by many consumers, or data you can't lose on a Redis failover
In the interview
At this volume, Redis Streams is enough; we'd move to Kafka if we needed replay or long retention.
- Treating a memory-first queue as durable for jobs you can't lose.
Segmented logs and the high-water mark
Situational · how logs work insideHow a replicated log (a Kafka partition, a Raft log) is stored and what readers are allowed to see.
How it works
- Segmented log: the log is a series of files (segments), each a few hundred MB to 1 GB, plus an index from offset to position. New messages go to the active segment; retention deletes or compacts whole old segments, which is cheap.
- High-water mark: the highest offset that has reached every in-sync replica. Consumers may only read up to it, so a message they've seen can't vanish if the leader dies before followers copy it.
- Log compaction keeps only the latest value per key, turning a topic into a durable table of current state.
Status today
Kafka, Redpanda, Pulsar's BookKeeper and Raft implementations all work this way; Kafka exposes it as consumer lag and the in-sync replica set.
Use it when
- Infrastructure interviews: design a message queue or a replicated log
- Explaining why acks=all and min.insync.replicas matter
Skip it when
- Product designs: "Kafka, replication factor 3" is enough
In the interview
Consumers read only up to the high-water mark, so nothing they process can be lost on a leader failover.
- Assuming consumers see a message the moment the leader writes it. They see it once it's replicated.
6. Remember it
How this hook works: what "Clip the ticket, cook it twice safely" means
What the slogan means, phrase by phrase
- "Clip the ticket"
- The producer sends a small message and returns immediately; the broker stores it durably.
- "cook it"
- A consumer takes it when ready, processes it, and only then acknowledges it.
- "twice safely"
- Crashes between processing and acknowledging cause redelivery, so processing must be idempotent: a duplicate ticket mustn't produce a second dish.
Why this picture fits: an order rail is a queue you've seen working under pressure: rushes lengthen it, dropped dishes go back on it, and nobody waits at the pass for one cook.
The story → the concept
| In the kitchen | In the system |
|---|---|
| Waiters clipping tickets | Producers sending messages |
| The rail | The broker: a queue or a log |
| Cooks taking tickets | Consumers (competing workers) |
| A ticket back on the rail after a dropped dish | Redelivery after a failed or unacknowledged message |
| A ticket nobody can read, set aside for the manager | A dead-letter queue |
| Keeping every ticket of the night | An event log with retention and replay |
Watch it on an example
- Checkout publishes
OrderPlaced {order_id: 881}and returns "Order confirmed" in 40 ms. - The email worker sends the receipt, then crashes before acknowledging. The message is redelivered.
- The second attempt checks "receipt already sent for order 881?" and skips: no duplicate email.
- The analytics consumer reads the same event from the log an hour later, at its own pace.
The trap it saves you from
Believing a broker's "exactly-once" claim covers your side effects. Emails, payments and API calls outside the broker are only safe if your handler is idempotent.
It still works when…
The "messages" are database changes (CDC, Module 13) or workflow steps (Module 22): same rail, same rule.
7. Common mistakes
8. Design exercise (25 minutes)
- Pick the shape (queue, log or pub/sub) for: sending password-reset emails, streaming every order to analytics and search, telling all app servers to drop a cached setting, processing video uploads.
- Design an idempotent consumer for "charge the customer for order X". What do you store, and what happens on a duplicate?
- Choose keys: a bank streams account transactions through Kafka. What key guarantees per-account order? How many partitions for 50,000 events a second if one consumer handles 2,000?
- Size the workers in the simulator for 300 messages a second normally and a 10× burst, drained within 10 minutes.
- Write it down: the three delivery guarantees, and "assume duplicates".
9. Self-check quiz
With at-least-once delivery, what must consumers handle?
Where does Kafka guarantee message order?
What caps the parallelism of one consumer group in classic Kafka?
A message fails every time it's processed. What should happen?
Writes arrive at 30k/s on average and your database handles 20k/s. Does a queue in front fix it?
Clicks for one viral ad all land on one Kafka partition. What's the usual fix?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Turning the database inside out (Martin Kleppmann) | 45 min | The talk and essay on building systems around an append-only log of events, the idea behind Kafka-centred designs. |
| Apache Kafka documentation, design section | 1 h | Partitions, replication, consumer groups and delivery semantics from the source. |
| Amazon SQS Developer Guide | 45 min | Visibility timeouts, DLQs, FIFO queues and limits in a managed queue. |
| Designing Data-Intensive Applications, chapter 11 (Stream Processing) | 2 h | Message brokers vs logs, and how streams connect systems. |
11. Under the hood (optional)
How queues and logs really deliver, order, retry and store messages. Not needed for most interviews; open it when you want to know how the products really do it. All the deep dives are listed on the Under the hood page.
Messaging internalsQueues vs logs, delivery guarantees with a crash lab, ordering scopes, rebalancing, visibility timeouts and DLQs, segments and compaction, partition sizing, 8 products comparedabout 40 min
"Put it on a queue" hides real choices: does the message disappear once read or stay for replay, who decides how fast consumers receive, what happens when a consumer dies halfway, and what "exactly once" can honestly mean. Products answer these differently, and the differences shape designs.
Queue vs log
| Queue (SQS, RabbitMQ classic and quorum queues) | Log (Kafka, Kinesis, Pulsar, RabbitMQ streams, Redis Streams) | |
|---|---|---|
| After a message is handled | Deleted | Kept until retention expires; each consumer group tracks its own position (offset) |
| Many independent consumers | Need a copy each (fan-out through topics or exchanges) | Free: each group reads the same log |
| Replay after a bug fix | Gone | Rewind the offset and reprocess |
| Parallelism | Any number of workers take messages one by one | One consumer per partition per group (Kafka share groups, GA in 4.2, add queue-style consumption) |
| Per-message acks and retries | Native | A committed offset covers everything before it; one stuck message blocks its partition |
Push vs pull: RabbitMQ pushes messages to consumers, limited by a prefetch count of unacknowledged messages; Kafka and SQS consumers pull at their own pace (SQS long polling waits up to 20 seconds for messages). Pull makes back-pressure natural; push gives lower latency but needs the prefetch limit to avoid flooding slow consumers.
Delivery guarantees and "exactly once"
Whether a message is lost or processed twice depends on when the consumer acknowledges relative to its side effects, and where a crash lands. Try it:
- Exactly-once delivery over a network is impossible in general; exactly-once effect is achievable by making processing idempotent or by committing output and progress atomically.
- Inside Kafka: idempotent producers (on by default since 3.0) remove duplicates from producer retries; transactions write output records and the consumer's offsets atomically, so read-process-write within Kafka is exactly once. Effects outside Kafka (an email, a payment) still need idempotency.
- SQS FIFO deduplicates sends with the same deduplication ID within 5 minutes; Pub/Sub offers an exactly-once delivery option for pull subscriptions.
Ordering scopes
| Product | Order is guaranteed within | Pick the key that must stay in order |
|---|---|---|
| Kafka, Kinesis, Pulsar | One partition or shard | The message key (an order ID, a user ID) chooses the partition |
| SQS FIFO | One message group ID | Group by entity; different groups run in parallel |
| Pub/Sub | One ordering key (when ordering is enabled) | Same idea |
| RabbitMQ | One queue with one consumer | Several consumers or redeliveries reorder messages |
| SQS standard | Nothing (best effort) | Design consumers not to care, or use FIFO |
Retries break order too: if message 5 fails and goes to a retry queue while 6 succeeds, the entity sees 6 before 5. Where order matters per entity, pause that key or use a version check on the receiving side.
Consumer groups and rebalancing
- Partitions are divided among a group's members; a consumer that joins, leaves or misses heartbeats triggers a rebalance.
- The classic ("eager") protocol stopped the whole group while every partition was revoked and reassigned. Cooperative rebalancing moved only the partitions that had to move, and Kafka 4.0 made the broker-driven protocol from KIP-848 generally available, so assignments change incrementally without a group-wide pause.
- A consumer that processes too slowly between polls (
max.poll.interval.ms, 5 minutes by default) is considered dead and its partitions are taken away, so long jobs need smaller batches or a pause on the partition.
Acks, visibility timeouts, retries and dead letters
- SQS: a received message is hidden for the visibility timeout (30 seconds by default, up to 12 hours); if not deleted in time it reappears for another consumer. Extend the timeout for long jobs. After
maxReceiveCountreceives, a redrive policy moves it to a dead-letter queue. - Pub/Sub uses an ack deadline the same way; RabbitMQ redelivers unacknowledged messages when the consumer's channel closes, and quorum queues can cap redeliveries.
- Poison messages (that always fail) must leave the main path: a dead-letter queue, with alerts and a tool to inspect and replay.
- Retry with delay in a log: Kafka has no per-message delay, so failed messages go to retry topics (retry-1m, retry-10m) read by consumers that wait until each message is due.
- Consumer lag (newest offset minus committed offset, or the age of the oldest message) is the metric to alert and autoscale on.
Inside a log: segments, retention, compaction and speed
- Segments: each partition is a series of files (1 GB by default in Kafka) with offset and timestamp indexes. Retention deletes whole old segments by time or size, which is cheap. Tiered storage (Kafka 3.9+, Pulsar) moves old segments to object storage.
- Compaction keeps only the latest record per key (a tombstone, a null value, deletes the key): a topic becomes a changelog you can rebuild state from.
- Why it's fast: producers batch records (
linger.ms,batch.size) and compress whole batches; brokers append sequentially and serve reads from the operating system's page cache, sending file bytes straight to the socket with zero-copysendfile. - Schemas: a schema registry (Avro, Protobuf, JSON Schema) checks that new producer versions stay compatible (backward: new readers read old data; forward: old readers read new data), so producers and consumers can deploy independently.
- Across regions: MirrorMaker 2, Confluent Cluster Linking, Pulsar geo-replication; offsets differ between clusters, so consumers fail over by timestamp or translated offsets.
How many partitions?
Product by product
| Product | Model | Delivery and ordering | The detail worth knowing |
|---|---|---|---|
| Apache Kafka | Partitioned log on brokers; consumers pull; KRaft metadata (no ZooKeeper since 4.0) | Order per partition; idempotent producers and transactions for exactly-once inside Kafka | Share groups (4.2) add queue-style consumption; compaction keeps the latest value per key |
| Apache Pulsar | Stateless brokers over BookKeeper storage | Exclusive, shared, failover and key-shared subscriptions | Storage scales separately; geo-replication and tiered storage built in |
| RabbitMQ | Exchanges route to queues; push with prefetch | Quorum queues (Raft-replicated); streams for log-style use | Routing lives in the broker; classic mirrored queues were removed in 4.0 |
| Amazon SQS | Managed queue; long polling | Standard: at-least-once, best-effort order; FIFO: ordered per group, 5-minute deduplication | Visibility timeout and redrive to a dead-letter queue |
| Google Pub/Sub | Global topics and subscriptions, push or pull | Ordering keys; exactly-once delivery option; seek to replay | Ack deadlines work like visibility timeouts |
| Kinesis Data Streams | Shards with fixed capacity (1 MB/s or 1,000 records/s in, 2 MB/s out each) | Order per shard | Enhanced fan-out gives each consumer its own read capacity |
| NATS | Core pub/sub; JetStream adds persistent streams | At-most-once in core; at-least-once and deduplication windows in JetStream | Very low latency; wildcard subjects |
| Redis Streams | Append-only stream in memory | Consumer groups with a pending list per consumer; XAUTOCLAIM takes over stuck messages | Good for modest volumes already on Redis |
Say it in the interview
- "Do you get exactly-once?" Delivery is at-least-once; the consumer makes the effect exactly-once by deduplicating on the message ID in the same transaction as its write (or Kafka transactions when input and output are both Kafka).
- "Why not more consumers?" In a Kafka group, consumers beyond the partition count sit idle; partitions cap parallelism, so size them for peak (or use share groups).
- "What about a message that always fails?" Bounded retries with backoff, then a dead-letter queue with an alert and a replay tool, so it can't block the partition.
A consumer processes a message, then crashes before acknowledging it. What happens?
Kafka guarantees order within what?
An SQS consumer takes longer than the visibility timeout. What happens to the message?
Sources
- Apache Kafka documentation
- Kafka 4.2 upgrade notes
- KIP-848: the next-generation consumer rebalance protocol
- KIP-932: queues for Kafka
- Pulsar architecture
- RabbitMQ: quorum queues
- RabbitMQ: streams
- SQS: visibility timeout
- SQS FIFO: exactly-once processing
- Pub/Sub: exactly-once delivery
- Kinesis Data Streams quotas and limits
- NATS JetStream
- Redis Streams