Practice this topic in a realistic system design interview
When one service calls another directly and waits, the caller is only as fast and as reliable as the service it called. If that service is slow or down, the caller feels it immediately.
A message queue breaks that direct wait. Instead of calling another service immediately, a producer puts a message into a queue. Later, one or more consumers read the message and do the work.
This fits work that can happen in the background: sending a welcome email after signup, resizing an image, processing a payment event, or smoothing out a traffic spike. The queue sits between producers and consumers as a buffer. It gives the system room to absorb bursts, retry failures, and scale workers separately.
Loading simulation...
But a queue does more than improve performance. It changes how the system behaves. Work may happen later. Messages may be retried. Duplicates can happen. Ordering becomes a design choice. Backlogs become something the team must watch.
This chapter covers the core parts of a message queue and the problems it introduces.
A message queue is built from a few parts that move work from the services creating it to the services handling it. The diagram shows how they connect before we look at each part.
The producer creates messages and sends them to the queue. A producer should not need to know which consumer will process the message or whether that consumer is running right now.
Examples: API server, checkout service, upload service, scheduled job.
The consumer reads messages and does the work. Many consumers can read from the same queue at the same time.
Examples: email worker, payment worker, image processor, analytics pipeline.
A message is one unit of work sent through the queue. It usually contains:
orderId, userId, or event details.Messages should be small, clear, and versioned. Large files usually belong in object storage. The queue message should carry a reference to the file, not the whole file.
The queue stores messages until consumers can process them. Depending on the system, the queue may preserve order, support priorities, keep messages after they are read, or delete messages once they are acknowledged.
The broker is the system that owns queue storage and delivery. It accepts messages from producers, stores them, delivers them to consumers, tracks acknowledgments, and applies rules such as retries, retention, and dead lettering.
Examples include RabbitMQ, Amazon SQS, Google Cloud Pub/Sub, Azure Service Bus, and Redis Streams. Kafka is a distributed event log that is often used for queue-like workloads, but its model is different from a traditional broker.
A message follows a predictable path after a producer hands it to the broker. The important step is acknowledgment: the consumer tells the broker, "I finished this message." That is what lets the broker recover when a consumer crashes halfway through.
The basic flow looks like this:
Acknowledgment is the key reliability step. If the consumer crashes before acknowledging the message, the broker can deliver it again.
That retry behavior is useful, but it means consumers must assume the same message can arrive more than once.
Message systems support several patterns. Product names vary, but the design ideas are common.
A work queue spreads tasks across a group of consumers. Each message is normally processed by one consumer.
Use it when many workers can perform the same job, such as sending emails, generating reports, processing uploads, or running other background jobs.
Adding more consumers increases how much work you can process, until something else becomes the limit, such as the database, an external API, or a rate limit.
One published event can be copied to several independent subscribers. Each subscriber reacts in its own way.
In publish/subscribe, a producer publishes an event to a topic. Multiple independent subscribers can receive the event.
Use it when several parts of the system need to react to the same fact. An OrderPlaced event might update inventory, analytics, notifications, and fraud checks. A UserCreated event might trigger an onboarding email, CRM sync, and audit logging. A PaymentCaptured event might update billing, shipment, and customer history.
Pub/sub is useful because the producer does not need to know every service that cares about the event.
A priority queue processes urgent messages before less urgent messages. Use it when some work should jump ahead, such as password reset emails before marketing emails, paid customer exports before free-tier exports, or fraud alerts before routine analytics tasks.
Priority can make lower-priority work wait longer, so it should be used carefully.
A delayed queue hides a message until a delay passes or a scheduled time arrives. Common uses include retrying later, checking trial expirations, sending reminder emails, and handling timeouts.
Not every broker supports this directly. Some systems implement it with scheduled jobs, delay topics, or separate retry queues.
When a message uses up its retries, it moves off the main queue into a separate place for investigation.
A dead letter queue stores messages that failed too many times. It keeps bad messages from blocking the main queue and gives engineers a place to inspect, fix, replay, or delete them.
Message queues are useful when the producer and consumer should not have to move in lockstep.
The producer does not need to know which consumer handles the work. This makes it easier to change, scale, or restart consumers without changing producer code.
This does not remove the contract between systems. The message format becomes the contract.
The user-facing path can finish quickly while slower work runs in the background.
For example, an upload API can store the original file, enqueue an image-processing job, and return immediately. Workers can generate thumbnails after the request has completed.
Queues absorb bursts. If 100,000 jobs arrive in one minute but workers can process only 10,000 per minute, the queue lets the system catch up over time instead of failing immediately.
This only helps when delayed processing is acceptable. If users need an answer right now, a queue does not remove that requirement.
If a worker crashes or another service times out, the message can be retried. This is one of the main reasons queues are used for important background work.
Retries need limits and monitoring. Infinite retries can turn one bad message into a permanent drain on worker capacity.
Producers and consumers can scale separately. If image uploads spike, add more image workers. If email volume drops, run fewer email workers.
The real limit may still be somewhere else. More workers will not help if all of them are waiting on the same database lock or third-party rate limit.
Use a message queue when background processing is acceptable and the system benefits from buffering, retry, or independent scaling.
Good fits include:
Avoid adding a queue when:
A queue can make a system more resilient, but it can also hide failure. A request may look successful because the message was accepted, while the actual work fails minutes later. The product must be designed for that delay.
Message queues solve real problems, but they introduce new ones.
Most production systems should assume at-least-once delivery. In plain terms: a message may be delivered more than once if a consumer crashes after doing the work but before acknowledging it.
Consumers should be safe to retry. Processing the same message twice should not charge a customer twice, send duplicate refunds, or corrupt state.
Ordering is usually guaranteed only within a narrow boundary, such as a single queue, partition, message group, or key.
If strict ordering matters, group messages by the thing that must stay ordered, such as orderId or accountId. Global ordering rarely scales well.
A growing queue means consumers are falling behind. That may be fine for a short spike, but it becomes a production issue when messages get older than the business can tolerate.
Queue depth alone is not enough. Also track the age of the oldest message and how far consumers are behind.
If another service is unhealthy, thousands of workers retrying aggressively can make recovery harder.
Use backoff, jitter, rate limits, and circuit breakers where appropriate. In simpler terms: retry more slowly, add randomness, and stop hammering a service that is already failing.
Once other services consume your messages, changing the message format becomes a compatibility problem.
Use versioned formats, additive changes, and clear ownership. Do not remove or rename fields without a migration plan.
Running a queue well comes down to a handful of habits around message design, consumer behavior, and operations. They keep messages manageable, make retries safe, and make sure someone can recover the system when delivery goes wrong.
Different systems make different trade-offs. Pick based on the workload, not just popularity.
| System | Best Fit | Notes |
|---|---|---|
| RabbitMQ | Traditional work queues and routing | Mature broker with exchanges, routing keys, acknowledgments, retries, and DLQ support. Good when you need careful routing rules. |
| Amazon SQS | Managed cloud queues | Simple, highly scalable managed queue. Good for background jobs and separating AWS services. FIFO queues are available when ordering is needed within message groups. |
| Google Cloud Pub/Sub | Managed pub/sub and event intake | Good when one event should be sent to many services on Google Cloud. Uses topics and subscriptions instead of one classic queue. |
| Azure Service Bus | Enterprise messaging on Azure | Supports queues, topics, subscriptions, sessions, scheduled delivery, and dead-letter queues. |
| Apache Kafka | Event streams and long-lived logs | Stores ordered records in partitions and lets consumers track where they are in the stream. Excellent for event streams and replay, but not a drop-in replacement for every simple task queue. |
| Redis Streams | Lightweight stream processing | Useful when Redis is already in the architecture and the workload fits how Redis stores and keeps data. |
The most common mistake is choosing a system before defining the workload. A queue for email jobs, an event stream for analytics, and a workflow engine for multi-step business processes are related tools, but they are not the same tool.
Message queues let producers hand work to consumers for later processing. They help systems absorb bursts, retry failures, run background work, and scale producers and consumers separately.
The core trade-off is that work no longer happens immediately. You must design for delayed processing, duplicate delivery, partial failure, monitoring, and replay.
Use message queues when background work is acceptable and the buffer gives you real value. Keep the message contract clear, make consumers safe to retry, monitor backlogs, and treat the queue as a production system, not a hidden implementation detail.
10 quizzes