The At-Least-Once Default
When you put a message on a queue and a consumer picks it up, it’s natural to assume the message gets processed once. That assumption is almost always wrong, and the systems don’t hide it — they just state it in documentation that’s easy to skim past. Most message queues and event systems guarantee at-least-once delivery, which means exactly what it says: each message will be delivered one or more times. Not exactly once. At least once. The difference is the entire problem.
The reason at-least-once is the default is that exactly-once is extraordinarily hard to provide, and providing it end to end may be impossible in the general case. Consider what has to happen for a message to be processed once and only once: the consumer receives it, does the work, and acknowledges completion, and all three of those steps have to be atomic with respect to failure. But they can’t be. The consumer might do the work and then crash before acknowledging, in which case the queue — having never heard back — redelivers the message, and the work happens again. The only way to avoid redelivery would be to acknowledge before doing the work, but then a crash loses the message entirely. At-least-once is what you get when you’d rather process twice than not at all, which is the right default for almost everything.
This means the burden of correctness moves from the delivery system to the consumer. The queue promises the message will arrive; it makes no promise it won’t arrive twice, so the consumer has to be built to handle a message it has already seen. This is the same idempotency requirement that retries impose, arriving from a different direction — and it’s why idempotency keeps showing up as the load-bearing property in distributed systems. Whether the duplication comes from a client retrying a request or a queue redelivering a message, the defense is the same: make processing the same message twice have the same effect as processing it once.
The practical shape of the defense is usually deduplication on a stable message identifier. Each message carries an ID that stays the same across redeliveries, the consumer records which IDs it has processed, and when a message arrives with an ID already in the record, the consumer skips the work and acknowledges. This is the idempotency-key pattern wearing different clothes, and it works for the same reason: it moves the source of truth about “has this happened?” from the unreliable delivery path to a place the consumer controls. The record of processed IDs becomes the authority, and the queue’s redelivery becomes harmless.
The mistake to avoid is reaching for a system that claims exactly-once and trusting the claim without reading what it actually covers. Some systems do offer exactly-once semantics, but usually within a specific boundary — within a single stream, or for the internal state of a stream processor — and not across the boundary to your external side effects. The database write, the email, the charge to a card: those still happen inside your consumer, and the exactly-once guarantee typically doesn’t extend to them. The safe assumption is at-least-once everywhere, and idempotent consumers on top. Assume the message will come twice, build so that’s fine, and the guarantee the system does or doesn’t provide stops being something you have to bet correctness on.