AlgoInsight

System Design

Message Queues: What They Are and When to Use Them

A message queue is one of the most useful building blocks in distributed systems - it lets parts of a system talk without waiting on each other. Here's what queues do, how they work, and when to reach for one.

By Admin2026-06-1813 min readmessage-queuedistributed-systemsasynchronoussystem-designkafkaarchitecture

When two parts of a system need to communicate, the simplest approach is a direct call: service A calls service B and waits for a response. That works until it doesn't - until service B is slow, or overloaded, or temporarily down, and service A is left waiting, failing, or piling up requests it can't complete. A message queue is the building block that solves this. It lets one part of a system hand off work to another without waiting, decoupling them so each can operate at its own pace.

Message queues are everywhere in distributed systems, and understanding them is essential. This guide explains what a message queue is, how it works, the problems it solves, and when you should (and shouldn't) reach for one.

What a message queue is

A message queue is a component that sits between a sender and a receiver and holds messages until they can be processed. The sender (called the producer) puts a message into the queue and immediately moves on - it doesn't wait for the message to be handled. The receiver (called the consumer) takes messages from the queue and processes them when it's ready.

The queue is a buffer in the middle. The producer and consumer never talk directly; they only interact with the queue. This simple indirection is what makes queues so powerful - it breaks the tight, synchronous coupling between the two sides.

graph LR
    P[Producer<br/>drops message, moves on] --> Q[(Message Queue<br/>holds messages in order)]
    Q --> C[Consumer<br/>processes when ready]
This diagram shows the basic structure of a message queue. A producer drops a message into the queue and immediately moves on without waiting. The queue holds messages in order. A consumer pulls messages from the queue and processes them when it's ready. The producer and consumer never communicate directly - only through the queue.

A useful way to picture it: a message queue is like a restaurant's order ticket rail. Servers (producers) clip orders onto the rail and go back to their tables without waiting. Cooks (consumers) take tickets off the rail and prepare them at their own pace. The servers aren't blocked waiting for food, and the cooks work through the tickets steadily. The rail decouples the front of house from the kitchen.

The problems message queues solve

Message queues exist to solve several related problems that show up constantly in distributed systems.

Decoupling. Without a queue, the producer must know about the consumer and wait for it. With a queue, neither side depends on the other being available at the same moment. The producer can keep working even if the consumer is temporarily down - messages simply wait in the queue until the consumer comes back. This independence makes systems far more resilient.

Asynchronous processing. Some work is slow - sending emails, processing images, generating reports. If a user's request had to wait for all that to finish, the experience would be painful. Instead, the request handler drops a message on a queue ("send this email") and instantly returns to the user, while a consumer handles the slow work in the background. The user isn't kept waiting on work that doesn't need to block them.

Smoothing out load spikes. When traffic suddenly surges, a queue absorbs the burst. Producers can pile messages into the queue faster than consumers process them, and the consumers work through the backlog steadily without being overwhelmed. The queue acts as a shock absorber, protecting downstream systems from spikes that would otherwise crash them. This is one of the most valuable properties of a queue at scale.

Reliability. A good message queue holds onto messages durably until they're successfully processed. If a consumer crashes midway, the message isn't lost - it goes back on the queue to be retried. This means work doesn't vanish when something fails.

Tip: Emeka's note from production: The load-smoothing property is the one I lean on most. When you ingest billions of events a day, traffic is never smooth - it spikes hard when a popular title drops and everyone presses play at once. A queue in front of the processing lets the system absorb that surge gracefully: events pile into the queue during the spike, and the consumers drain it steadily afterward. Without that buffer, a spike would slam directly into the processing layer and take it down exactly when the most people are watching. The queue turns a potential outage into a brief, harmless backlog.

How processing works

When a consumer takes a message from a queue and processes it, there's an important detail: the message shouldn't be removed permanently until processing succeeds. Most queues handle this with an acknowledgment mechanism - the consumer processes the message and then sends an "ack" confirming success, at which point the queue discards it. If the consumer fails before acknowledging (it crashes, or processing errors out), the queue makes the message available again so another consumer can retry it.

This gives an important guarantee: messages aren't lost just because a consumer failed. But it introduces a subtlety - if a consumer processes a message but crashes before acknowledging, the message gets redelivered and processed again. This means messages can sometimes be processed more than once, so consumers should ideally be idempotent (processing the same message twice produces the same result as processing it once). Designing for this "at-least-once" reality is a key part of working with queues correctly.

You can also scale consumers easily: add more consumers pulling from the same queue, and they share the work, each taking different messages. This is a natural way to scale up processing - if the queue is backing up, add consumers to drain it faster.

A worked example: the upload that doesn't make you wait

Let me trace a concrete case. A user uploads a photo, and the system needs to do several things: store the photo, generate thumbnails in multiple sizes, scan it for inappropriate content, and update the user's gallery. Done synchronously - all in the request - the user would stare at a spinner for several seconds while all that work completes. Slow, fragile (if thumbnail generation hiccups, the whole upload fails), and it ties up a web server for the entire duration.

With a message queue, it works differently. The upload handler does the minimum needed to respond - saves the original photo and drops a message on a queue ("process photo #12345") - then immediately returns success to the user. The user's upload feels instant. Behind the scenes, consumers pull that message and do the slow work: one generates thumbnails, content scanning runs, the gallery updates. If thumbnail generation fails, that message is retried without affecting the user's upload, which already succeeded. And when a thousand users upload at once, the messages queue up and the consumers work through them steadily, rather than a thousand simultaneous requests crushing the servers.

This is the queue's value in one example: the slow work is decoupled from the user's request, failures are isolated and retried, and spikes are absorbed. The user gets an instant response; the system stays resilient.

Delivery guarantees: at-most-once, at-least-once, exactly-once

An important dimension of message queues is what delivery guarantee they provide - how they behave when things go wrong. There are three levels:

Most real systems use at-least-once delivery and make their consumers idempotent to handle the occasional duplicate, because true exactly-once is expensive. Knowing which guarantee you have - and designing consumers accordingly - is essential to using queues correctly.

Dead letter queues: handling messages that won't process

What happens when a message simply can't be processed - it's malformed, or it triggers a bug, and every retry fails? Without a plan, that message gets retried forever, clogging the queue and wasting resources. The standard solution is a dead letter queue (DLQ): after a message fails a set number of times, it's moved out of the main queue into a separate "dead letter" queue, where it can be inspected and handled later without blocking everything else.

The DLQ is a safety valve. It keeps a few poison messages from jamming the whole pipeline, while preserving them for debugging rather than silently dropping them. Any production queue system should have a DLQ strategy - it's one of those details that separates a robust messaging setup from one that mysteriously stalls when a bad message arrives.

Ordering and scaling consumers

Two practical considerations come up constantly. Ordering: within a single queue, messages are typically processed in the order they arrived (FIFO - first in, first out), but the moment you add multiple consumers pulling in parallel, strict global ordering gets tricky, because different consumers finish at different speeds. If order matters (process a user's actions in sequence), systems often use partitioning - routing all messages for a given key (say, a user) to the same consumer so their relative order is preserved, while still parallelizing across different keys. Scaling: one of the queue's best properties is how naturally it scales. If the queue is backing up (producers outpacing consumers), you simply add more consumers, and they share the growing backlog. Monitoring queue depth is one of the most useful operational signals in a queue-based system: a steadily growing queue means consumers can't keep up and you need more of them.

Common message queue systems

You rarely build a message queue from scratch - you use an established system. The well-known ones include RabbitMQ (a flexible message broker), Amazon SQS (a fully managed cloud queue), Apache Kafka (a high-throughput, log-based system used for both queuing and streaming at massive scale), and Redis (lightweight queuing alongside its other features). They differ in their guarantees, throughput, ordering behavior, and operational model, but all provide the core capability: durable, reliable handoff of messages between producers and consumers. The throughline across all these details - guarantees, dead letter queues, ordering, scaling - is that a queue trades a little added complexity for a lot of resilience and flexibility. For the many workloads that can be done asynchronously, that's an excellent trade.

When to use a message queue

Reach for a message queue when:

When NOT to use a message queue

A queue isn't always the right tool:

Interview: Message queues come up constantly in system design. The strongest use is naming the specific problem the queue solves: "I'll put the image-processing behind a queue so the upload returns instantly and the heavy work happens asynchronously - and the queue absorbs spikes so a flood of uploads doesn't overwhelm the processors." Mentioning that consumers should be idempotent because messages can be redelivered shows you understand how queues actually behave, not just what they're for.

Queues vs event logs: a related distinction

One more distinction worth knowing, because it shapes tool choice. A traditional message queue removes a message once it's consumed - delivered, acknowledged, gone. A newer style, the event log (exemplified by Kafka), instead keeps messages in an ordered log for a retention period, and consumers track their own position in the log. This means multiple independent consumers can each read the same messages (more like pub-sub), and a consumer can "rewind" to reprocess past events - impossible with a traditional queue where messages disappear after consumption. For pure work distribution, a traditional queue is simpler and sufficient. For high-throughput event streams that multiple systems consume and may need to replay, a log-based system fits better. The line between "message queue" and "event streaming platform" blurs here, and which you choose depends on whether you need messages to persist and be re-readable, or simply to be handed off once and forgotten.

The bottom line

A message queue is a buffer that sits between a producer and a consumer, holding messages so the two sides don't have to interact directly or at the same time. It decouples components, enables asynchronous background processing, absorbs traffic spikes, and provides reliable handoff where failures lead to retries rather than lost work. The trade-offs are added infrastructure complexity and the need to handle at-least-once delivery (make consumers idempotent), and it's the wrong tool when you need an immediate synchronous response.

Used well, a queue is one of the most valuable building blocks for making distributed systems resilient and scalable - which is why they sit at the heart of so many large systems. We cover queues and asynchronous architecture across System Design Fundamentals.


Go deeper

More articles