Skip to content

Writing

At least once is the only honest guarantee

Exactly once delivery is sold constantly and cannot be delivered across a network. The useful move is to stop wanting it, because the thing you actually need is achievable and much simpler.

The thing that cannot be sold

Two systems, a network between them. A sends a message. B receives it and acts. B sends back an acknowledgement. The acknowledgement is lost.

A now has to choose. Send again, and B might act twice. Do not send again, and B might never have acted at all.

There is no third option, and no amount of protocol removes the choice, because the failure is that A cannot distinguish “B never received it” from “B received it and the reply was lost”. Those two states look identical from where A is standing, and they always will.

Any system claiming exactly once is doing one of two things: giving you at least once with deduplication somewhere you cannot see, or restricting the guarantee to inside its own boundary where it controls both ends. The second one is real and it evaporates the moment the effect leaves that boundary, which for anything interesting it does.

The reframe that fixes it

Stop trying to make the message arrive exactly once. Make acting on it twice have the same result as acting on it once.

That is a local problem, entirely within your control, and it is solvable. It does not require coordination, consensus, or trusting anybody’s marketing.

Once you have it, the whole anxiety dissolves. Retry freely. Replay a day of events. Kill a worker mid run. None of it matters, because the second run is a no operation.

Identity is the whole of it

Idempotency is not a library and it is not a flag. It is one decision: what makes two operations the same operation.

Get that right and the rest is bookkeeping. Get it wrong and you have a system that looks idempotent and is not, which is worse than one that obviously is not, because you will trust it.

The rules I now apply without thinking:

Use an identifier that is stable across retries. For a webhook that is the provider’s own reference. For a scheduled job it is the thing the work is for, meaning this customer for this period, not the moment the job fired. A fired-at key makes every retry look like new work, which is exactly backwards.

Never use a hash of the payload. This is the trap I want to warn people about most, because it looks correct and it fails late. Providers reformat payloads. A field order changes, a null becomes an empty string, an integer arrives as a string. The hash changes. The event looks new. You act again. Your idempotency worked perfectly in testing and broke silently after somebody else’s deploy.

Include the tenant. In a multi-tenant system, two customers can legitimately produce the same reference. Without the tenant in the key, one customer’s operation silently deduplicates against another’s, and the symptom is that something quietly never happened, with no error, pointing nowhere near the cause.

Claim, do not rely on the constraint

The natural instinct is a unique index. Have one. Do not make it the mechanism.

A unique index protects the row and nothing else. The second attempt still updates the balance, still emits the event, still sends the message, and fails only at the very end. It also fails with an error, which most callers interpret as a failure and answer by retrying harder. Your protection has become the source of the load.

Claim the key first, in its own small transaction, and if the claim is already taken, return success without doing anything.

INSERT INTO processed (key) VALUES ($1) ON CONFLICT DO NOTHING RETURNING id

nothing returned  ->  already handled, return 200, do no work

The second arrival now does nothing and reports success, which is precisely what a retrying caller needs to stop. The unique constraint stays underneath as the backstop for the race you did not anticipate.

The part everybody skips

At least once handles duplicates. It does nothing about the opposite failure: an operation that was claimed and never completed, because the process died in between.

That one is invisible. The caller received its acknowledgement and will not retry. No error was raised. Nothing is in a queue. It simply never finished, and it will sit there forever.

The only thing that finds it is a sweep that looks for work claimed but not completed, and reprocesses it. Because the work is idempotent, reprocessing is safe.

every 2 minutes:
  find claimed more than 5 minutes ago and not done
  reprocess

This is what converts a dead worker from a correctness problem into a latency problem, and it is routinely left out because the happy path works fine without it.

And the one after that

Even the sweep cannot detect an operation that never arrived at all. Absence generates no signal.

For that you need a comparison against an external source of truth, on a slower cycle. A daily reconciliation against the provider’s own statement. That is the only mechanism that can find something that is missing rather than something that is wrong.

Three layers, and each catches what the previous one structurally cannot:

claim key       catches duplicates
sweep           catches crashed workers
daily compare   catches events that never arrived

How to know it works

Not a unit test. Replay a day of real events, three times, in random order, killing the worker at random points.

The resulting state must be identical after every run. The count of side effects must equal the count of distinct operations.

If that holds, you have at least once with idempotent handling, which is the strongest honest guarantee available. If it does not, the failure is almost always the claim step or the missing sweep.

Why this is better than what you wanted

Exactly once, even if you could buy it, would be a guarantee you have to trust. This is a property you can test, on your own machine, in an afternoon.

I would rather have a property I can prove than a guarantee somebody has promised me.

Published May 2026, updated July 2026.

Hiring someone to own work like this?

I am open to senior backend and full stack roles, remote and permanent.

Get in touch / Read the CV