When a user clicks "place order", they shouldn't have to wait for the confirmation email to be sent, the invoice to be generated and the supplier's inventory to be notified — all inside that single request. Asynchronous processing with queues is how you take that weight off the request and keep the system responsive and decoupled. Understanding producer, consumer and reliable delivery is expected of a mid-level dev.
Why process asynchronously
An HTTP request that does five things takes as long as all five added together — and if the third one fails, the user gets an error even though the order was saved. The idea behind async is simple: the request does only the essentials (saving the order) and publishes the rest as tasks to be done later. The user gets a response in milliseconds; the heavy work happens in the background.
The analogy: in a restaurant, the waiter doesn't cook your dish in front of you. He writes down the order (publishes), pins the ticket to the counter (queue) and the kitchen pulls tickets at its own pace (consumer). The waiter immediately goes back to serving other tables.
Producer and consumer
The producer is whoever creates the message and puts it on the queue. The consumer is whoever takes the message off and processes it. They don't know each other and don't need to be online at the same time: the producer publishes and moves on; the consumer processes when it can. The queue (run by a broker such as RabbitMQ, SQS or Kafka) sits in the middle, holding the messages.
// Producer (inside the request)
order = saveOrder(data)
queue.publish("orders.created", { id: order.id })
respond(202, "order received")
// Consumer (separate process)
on queue.receive("orders.created") as msg:
sendEmail(msg.id)
msg.ack() // confirms processing
The message's path
Producer Queue (broker) Consumer
┌────────┐ publish ┌──────────────┐ pull ┌──────────┐
│ request│ ───────▶ │ [m3][m2][m1] │ ────▶ │ process │
└────────┘ └──────────────┘ └────┬─────┘
▲ buffer │ ack
└───────────────────────┘
The queue works as a buffer: if a thousand orders arrive in a spike, they pile up and the consumers drain them at their own pace, without taking the system down. This smooths out spikes and connects directly with scalability — just spin up more consumers to process faster.
Queue vs topic (pub/sub)
In a traditional queue, each message is delivered to one consumer — multiple consumers share the work. In the pub/sub pattern (topic), each message is delivered to all interested subscribers. The difference changes the design of the system:
| Model | Delivery | Typical use |
|---|---|---|
| Queue | 1 message → 1 consumer | Distributing work (work queue) |
| Topic (pub/sub) | 1 message → N subscribers | Notifying events to several services |
"Order created" as an event on a topic lets inventory, billing and email react independently — each with its own subscription.
What changes when you decouple services
Without a queue, the orders service calls the email service directly; if email goes down, orders breaks too. With the queue in the middle, the producer only depends on the broker. The consumer can be offline and the messages just wait — when it comes back, it processes the backlog. This decoupling is a central piece of resilience: one service's failure stops taking down the others.
Delivery, reprocessing and dead-letter
Since the consumer can fail halfway through processing, it only confirms the message with an ack after finishing. Without an ack, the broker assumes something went wrong and redelivers the message. This guarantees nothing gets lost — but it means the same message may arrive more than once (at-least-once delivery). A message that fails repeatedly goes to a dead-letter queue (DLQ): a separate queue for manual inspection, preventing a "poison message" from blocking the consumer forever.
Ordering and idempotency
Two classic pitfalls. First: queues generally do not guarantee global ordering; if order matters, use a partition key or accept reordering. Second, and more important: since the same message can be delivered twice, the consumer needs to be idempotent — processing the same message N times must have the same effect as processing it once. In practice, this becomes "check whether the email was already sent for this order before sending it".
The queue promises to deliver the message at least once — it's up to the consumer not to mess everything up when it arrives twice. Idempotency isn't a detail: it's what separates a robust messaging system from one that sends the email three times.
Conclusion
Queues take heavy work off the request, smooth out spikes and decouple services so that one's failure doesn't bring down the others. The price is having to think about delivery (ack and DLQ), ordering and idempotency. Combined with caching, scalability and resilience, messaging is what lets systems grow without every part having to wait for the others.