Ecommet
Topic 40 of 75

Message Queues and Delivery Guarantees

A message queue lets one part of a system hand work to another without waiting for it. Learn how queues decouple services, how acknowledgements and visibility timeouts work, and what at-most-once, at-least-once and exactly-once really mean.

Intermediate 6 min read 4 quiz questions

Jump to the quiz

In a nutshell

  • A queue sits between producers and consumers: producers send and move on, consumers process at their own pace.
  • Queues decouple services, absorb spikes and let you add consumers to process more in parallel.
  • A consumer acknowledges a message after processing; unacknowledged messages are delivered again.
  • Most queues give at-least-once delivery, so consumers must be idempotent. "Exactly-once" is at-least-once plus deduplication.

Why put a queue in between

When a customer places an order, the checkout must save the order and take payment. Sending the confirmation email, updating analytics, notifying the warehouse and generating an invoice PDF can all happen a few seconds later. If checkout calls each of them directly, it is only as fast and as reliable as the slowest one.

Checkout hands off work through a queue

Checkout hands off work through a queue Customer → Checkout: place order Checkout → Queue: send message Queue → Email worker Queue → Invoice worker Queue → Warehouse CustomerCheckoutQueueorder.placedEmail workerInvoice workerWarehouseplace ordersend message

All steps Checkout returns as soon as the message is stored. Workers pick messages up when they are ready.

Diagram as text
  1. The customer places an order; checkout saves it and takes payment.
  2. Checkout puts a message on the queue and immediately replies "order confirmed".
  3. Workers process the follow-up tasks in the background, each at its own pace.
  • Faster responses: the user does not wait for slow follow-up work.
  • Resilience: if the email service is down, messages wait in the queue instead of failing checkout.
  • Load levelling: a burst of 10,000 orders is absorbed by the queue and processed at a steady rate.
  • Scaling: add more consumers to drain the queue faster (the competing consumers pattern).

Acknowledgements and visibility timeouts

A consumer that takes a message might crash before finishing it. So queues do not delete a message when it is handed out. Instead it becomes invisible for a while (the visibility timeout, in Amazon SQS terms) or is marked unacknowledged (RabbitMQ). The consumer acknowledges it after processing; only then is it deleted. If no acknowledgement arrives in time, the message is delivered again.

A tiny queue with visibility timeouts: unacknowledged messages come back

Java
import java.util.*;

class VisibilityQueue {
    private final double timeoutSeconds;
    private final Deque<String> ready = new ArrayDeque<>();
    private final Map<String, Double> inFlight = new LinkedHashMap<>(); // id -> visible again at
    private final Map<String, String> bodies = new HashMap<>();
    private int nextId = 1;

    VisibilityQueue(double timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }

    synchronized String send(String body) {
        String id = "m" + nextId++;
        bodies.put(id, body);
        ready.addLast(id);
        return id;
    }

    // Returns {id, body}, or null if nothing is available.
    synchronized String[] receive(double now) {
        Iterator<Map.Entry<String, Double>> it = inFlight.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Double> e = it.next();
            if (now >= e.getValue()) { // timed out: make it visible again
                ready.addLast(e.getKey());
                it.remove();
            }
        }
        String id = ready.pollFirst();
        if (id == null) {
            return null;
        }
        inFlight.put(id, now + timeoutSeconds);
        return new String[] { id, bodies.get(id) };
    }

    synchronized void ack(String id) {
        if (inFlight.remove(id) != null) {
            bodies.remove(id);
        }
    }
}
Python
from collections import deque

class VisibilityQueue:
    def __init__(self, timeout_seconds):
        self.timeout = timeout_seconds
        self.ready = deque()
        self.in_flight = {}  # id -> time it becomes visible again
        self.bodies = {}
        self.next_id = 1

    def send(self, body):
        msg_id = f"m{self.next_id}"
        self.next_id += 1
        self.bodies[msg_id] = body
        self.ready.append(msg_id)
        return msg_id

    def receive(self, now):
        """Returns (id, body), or None if nothing is available."""
        for msg_id, visible_at in list(self.in_flight.items()):
            if now >= visible_at:  # timed out: make it visible again
                del self.in_flight[msg_id]
                self.ready.append(msg_id)
        if not self.ready:
            return None
        msg_id = self.ready.popleft()
        self.in_flight[msg_id] = now + self.timeout
        return msg_id, self.bodies[msg_id]

    def ack(self, msg_id):
        if self.in_flight.pop(msg_id, None) is not None:
            del self.bodies[msg_id]

Delivery guarantees

GuaranteeHow it happensCan lose?Can duplicate?
At-most-onceAcknowledge before processing (or never retry)YesNo
At-least-onceAcknowledge after processing; redeliver on timeoutNoYes
Exactly-once (effectively)At-least-once delivery + idempotent or deduplicated processingNoNo visible effect

Why duplicates are unavoidable with at-least-once: the consumer processes the message, then crashes (or the network drops) before its acknowledgement arrives. The queue cannot tell this apart from a consumer that never processed it, so it redelivers.

Messages that fail again and again (a malformed payload, a bug) should not loop forever. After N attempts, move them to a dead-letter queue where a person or a tool can inspect them.

Ordering

With several consumers working in parallel, messages finish out of order even if the queue hands them out in order. If order matters only per entity (all events of order 1042 in sequence), route each entity's messages to one partition or message group, processed by one consumer at a time. Global ordering across everything does not scale and is rarely needed.

Common mistakes

  • Assuming each message is delivered exactly once, and charging or emailing twice on redelivery.
  • Acknowledging before processing, then losing the message when the consumer crashes.
  • A visibility timeout shorter than processing time, so messages are processed in parallel twice.
  • No dead-letter queue, so one poison message is retried forever.

Interview questions

Try answering out loud before you open each one.

How do you make sure an email is sent once for each order?

Use at-least-once delivery and an idempotent consumer: before sending, check a sent-emails table keyed by order ID (or message ID), and record the send in the same step, so a redelivered message is skipped. Retry failures with backoff and move repeated failures to a dead-letter queue.

Why use a queue between two services?

To decouple them in time and availability: the producer does not wait for or depend on the consumer being up. The queue absorbs bursts, lets you scale consumers independently, and retries failed work. The costs are eventual completion, duplicates to handle, and another system to run and monitor.

Check yourself

1. A consumer processes a message, then crashes before acknowledging. With at-least-once delivery…
Show the answer

Answer: The message is delivered again. Without an acknowledgement the queue assumes it was not processed.

2. How do you get exactly-once effects?
Show the answer

Answer: At-least-once delivery plus idempotent processing. Duplicates still arrive, but have no extra effect.

3. What is a dead-letter queue for?
Show the answer

Answer: Messages that keep failing, set aside for inspection. It stops poison messages from being retried forever.

4. Processing takes up to 60 s and the visibility timeout is 30 s. What happens?
Show the answer

Answer: The message may be handed to a second consumer while the first is still working. It reappears after 30 s because no acknowledgement arrived yet.

Further reading

Your notes

Saved in this browser as you type.

Finished this topic?

Mark it read to track your progress, then carry on with the next one.

Next topic