Book a 30-min callBook a call

Idempotency and job queues: at-least-once delivery, retries and exactly-once effects

Why queues deliver at least once, and how idempotency keys, dedupe tables, the outbox pattern and dead-letter queues give you exactly-once effects.

A queue of message blocks flowing into a worker box, a dashed loop arrow returning a duplicate, and a table with a tick beside the worker.
Written by
BYD Tech
Published
Topic
Deep dives
Reading time
13 min read

Questions people ask

Can a message queue guarantee exactly-once delivery?

Within its own walls, partly: Kafka’s idempotent producer and transactions, and SQS FIFO deduplication, stop some duplicates. But the effect you care about happens in your database or another service, so in practice you get at-least-once delivery and make the handler idempotent, which gives exactly-once effects.

How long should I keep idempotency keys?

Longer than any client will retry and longer than any redelivery or manual replay can happen. Stripe documents that its keys can be pruned once they are at least 24 hours old. Pick a period, document it, and treat a key seen after it as a new request.

Is a dedupe table enough without the outbox pattern?

It is enough when the handler’s only side effects are writes to the same database. If the handler also publishes an event or calls another service, you need the outbox or a downstream idempotency key, or a crash between the commit and the call loses or repeats that effect.

What should happen to messages in a dead-letter queue?

Someone should be alerted, read why each message failed, fix the cause, and redrive the messages to the source queue. Keep them longer than the source queue would, and make sure the handler is idempotent, because a redrive is a redelivery.

Do GET requests need an idempotency key?

No. GET, PUT and DELETE are idempotent by definition in HTTP, so repeating them has the same effect as sending them once. Keys are for POST and PATCH requests that create or change something.

Building something like this?