Cloud Tech by Victor
System DesignIntermediate

Message Queues & Event-Driven Architecture

Why a visibility timeout, not a delete, is what actually protects a message from being processed twice, and why "at-least-once delivery" means your consumer has to handle duplicates even when everything is configured correctly.

Updated 2026-07-243 min read

Overview

A message queue decouples producers from consumers, but the mechanism that actually prevents two consumers from processing the same message concurrently is the visibility timeout: once a consumer receives a message, it becomes temporarily invisible to other consumers, and only an explicit delete before that timeout expires removes it for good. If the timeout expires first, whether from a slow consumer, a crash, or a lost delete request, the message becomes visible again and gets redelivered, which is exactly what "at-least-once delivery" means: a message is guaranteed to be delivered and processed eventually, but not guaranteed to be delivered exactly once. That guarantee shape is deliberate, not a flaw, and it means any consumer built on this model has to be idempotent, processing the same message twice has to produce the same outcome as processing it once, because duplicate delivery is a normal, expected part of the model, not a rare failure case.

Quick Reference

ConceptWhat it does
Visibility timeoutHides a received message from other consumers while it's being processed
Manual/automatic deleteRemoves the message for good once processing succeeds
Timeout expires without deleteMessage becomes visible again, gets redelivered
At-least-once deliveryGuarantees eventual delivery; does not guarantee exactly one delivery

Syntax

python
messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1)
# process the message...
sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=messages['Messages'][0]['ReceiptHandle'])

Examples

python
# Idempotent processing - safe even if this message is delivered
# and processed more than once, which at-least-once delivery allows.
# claim_processing() atomically inserts order_id and returns False if
# it already exists, so two concurrent redeliveries can't both pass
# the check before either one claims it.
def process_order(message):
    order_id = message['order_id']
    if not claim_processing(order_id):
        return
    # idempotency_key ties the charge itself to this order, so even a
    # retry of charge_customer after a crash won't double-charge.
    charge_customer(order_id, idempotency_key=order_id)

At-least-once delivery means duplicates are expected, not rare

A consumer that assumes each message arrives exactly once will eventually double-process one, from a crash, a lost delete, or a timeout racing completion. Design every consumer to be idempotent rather than relying on the queue to prevent duplicates.

Visual Diagram

Common Mistakes

  • Assuming a message queue guarantees exactly-once processing, when the standard model (and the one most queues offer) is at-least-once, duplicates are expected.
  • Setting the visibility timeout far shorter than actual processing time, causing legitimate in-progress messages to be redelivered and double-processed unnecessarily.
  • Writing consumers that aren't idempotent, then being surprised by real-world duplicate side effects (double charges, duplicate emails) under normal operation, not just failure scenarios.
  • Not using a dead-letter queue for messages that repeatedly fail processing, letting a poison message cycle indefinitely instead of being captured for investigation.

Performance

  • A visibility timeout set too long delays legitimate retries after a real consumer crash, directly increasing end-to-end processing latency for affected messages.
  • A visibility timeout set too short causes unnecessary redelivery and duplicate processing work under normal, still-in-progress conditions, wasted compute, not a correctness issue if the consumer is properly idempotent.

Best Practices

  • Set the visibility timeout to comfortably exceed expected processing time, and extend it programmatically for genuinely long-running tasks rather than picking one static value blindly.
  • Design every consumer to be idempotent, since at-least-once delivery makes duplicate processing a normal, expected event, not an edge case.
  • Route messages that fail processing repeatedly to a dead-letter queue instead of letting them cycle indefinitely in the main queue.
  • Delete messages promptly after successful processing to keep the in-flight message count low and avoid unnecessary redelivery windows.

Interview questions

What is a visibility timeout, and what problem does it solve that simply having multiple consumers poll a queue would create?

When a consumer receives a message, the queue doesn't delete it immediately, it starts a visibility timeout during which that message is hidden from other consumers, so a second consumer polling the same queue won't also pick up and process the same message concurrently. The consumer is expected to finish processing and explicitly delete the message before the timeout expires; if it does, the message never reappears. Without this mechanism, multiple consumers competing for messages would routinely double-process the same one, exactly the race condition visibility timeouts exist to prevent.

A consumer receives a message, starts processing, but crashes before deleting it. What happens to that message, and why is this actually the desired behavior?

Once the visibility timeout expires without the message being deleted, it automatically becomes visible again in the queue and can be picked up by the same or a different consumer for another processing attempt. This is deliberate, not a bug: the alternative (a crashed consumer's message vanishing permanently) would silently lose data, whereas reappearing after timeout guarantees eventual processing at the cost of a possible duplicate attempt, which is exactly the trade-off "at-least-once delivery" describes. Setting the visibility timeout too short causes premature, unnecessary reprocessing of messages still legitimately being worked on; too long delays legitimate retries after a real crash.

What does "at-least-once delivery" actually guarantee, and what does it not guarantee, and why does that mean a consumer must be idempotent?

At-least-once delivery guarantees a message will eventually be delivered and processed at least one time, it does not guarantee exactly one delivery, the same message can legitimately be delivered and processed more than once, for example if a consumer's deletion request is lost after it already finished processing, or a visibility timeout expires just as processing completes. Because duplicate delivery is a normal, expected outcome of this model rather than a rare edge case, a consumer has to be written so that processing the same message twice produces the same end result as processing it once (an idempotent operation), rather than assuming the queue itself will prevent duplicates.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement