What you'll learn
Quick Answer
A queue lets one service hand work to another without waiting. The producer adds a message and returns immediately; a consumer processes it later. This decouples services and absorbs traffic spikes, at the cost of eventual rather than immediate consistency.
The problem, concretely
A user signs up. Your handler saves the record, sends a welcome email, generates a thumbnail, notifies analytics and syncs to a mail provider. Only then does it respond.
Three things are wrong with that. It is slow, because the user waits for work they do not care about. It is fragile, because if the email provider is down the whole signup fails even though the account could have been created. And it does not absorb bursts — a thousand simultaneous signups means a thousand concurrent email calls.
With a queue, the handler saves the record, puts a message on the queue, and responds. The user gets a fast confirmation. A separate worker picks the message up and sends the email, whenever it can.
If the email provider is down, the message waits and is retried. The signup already succeeded.
Producers, consumers and the two shapes
A producer puts messages in; a consumer takes them out and processes them. The queue holds them in between and is durable, so a restart does not lose them.
Two patterns cover most uses:
- Work queue — each message is handled by exactly one consumer. Add more consumers to process faster. This is the email-sending case.
- Publish/subscribe — each message goes to every interested subscriber. An
order.placedevent might be consumed independently by inventory, analytics and notifications, none of which know about each other.
Publish/subscribe is the same decoupling idea as the Observer pattern, across a network — see design patterns. The service publishing the event does not know or care who consumes it, so adding a new consumer requires no change to the producer.
Delivery guarantees, and why they force idempotency
This is the part tutorials skip and interviews probe.
Most queues offer at-least-once delivery. A message is delivered at least once, and possibly more than once. Duplicates happen when a consumer processes a message successfully but crashes before acknowledging it — the queue, having heard nothing, redelivers.
Exactly-once is very difficult in a distributed system and is usually approximated rather than genuinely provided.
So consumers must be idempotent: processing the same message twice must have the same effect as once. Sending a duplicate welcome email is embarrassing; charging a card twice is serious.
The practical technique is to give each message a unique ID and record processed IDs, ignoring repeats. Or make the operation naturally idempotent — "set status to shipped" is safe repeated, "add one to the count" is not.
Ordering is the other assumption to check. Many queues do not guarantee it across multiple consumers, so if message order matters, that must be designed for explicitly.
When processing fails
A message that cannot be processed — malformed data, a permanently missing record — will be retried forever if you let it. It blocks the queue or spins endlessly, and it is a classic production incident.
The standard answer is a dead letter queue: after a set number of failed attempts, move the message aside for inspection rather than retrying indefinitely. The main queue keeps flowing, and you have the failures in one place to examine.
Pair that with exponential backoff — wait longer between each retry. Retrying a failing external API every second makes its outage worse and yours longer.
Monitor queue depth. A growing backlog means consumers are slower than producers, and it is the earliest warning that something is wrong — often the only warning, since the user-facing side still looks healthy.
When to use one, and when not to
Good candidates: sending email and notifications, image and video processing, report generation, syncing to third-party systems, and anything where the user does not need the result immediately.
Poor candidates: anything whose result the user needs in the response. Queuing a password check and telling the user to come back later is not an improvement.
The cost is real. You now have another system to run and monitor, debugging spans two processes, and the system is eventually consistent — a user may see "account created" before the welcome email exists. That is usually fine, provided the interface does not promise otherwise.
For a student project, a queue is usually unnecessary and occasionally worth adding deliberately to learn it. If you do, be ready to explain idempotency and dead letter queues, because that is what distinguishes having used one from having understood it. See microservices vs monolith for the wider trade-off.
