Architecture

Idempotency: why the same message must not open an account twice

What idempotency is, why a distributed system needs it and how to ensure it in practice

Martin Tribula26 October 20267 min readČíst česky

Share this article
Article cover: Idempotency: why the same message must not open an account twice. Martin Tribula – Architecture.

A timeout does not mean nothing happened. Without idempotency, a second attempt leads to a double payment, two accounts or the same e-mail sent to a customer twice.

Idempotency is the property that lets a system safely try the same thing again.

In a distributed system we will sooner or later receive the same request twice. The question is not whether it happens, but what happens to the data when it does.

Contents

What idempotency is

An operation is idempotent when performing it repeatedly has the same result as performing it once.

The difference between two commands shows it best:

The first command is idempotent, the second is not. A lift button works the same way: when I press it a second time, the lift does not come twice.

In software we do not care about the mathematical definition but about the effect on the state of the system. An idempotent operation may return a different response or write a log entry. What matters is that the second run does not change the data differently from the first.

Why it matters in distributed systems

As long as everything runs in one process and one transaction, we can see whether an operation went through. As soon as a network sits between two parts of the system, we lose that certainty.

Duplicates have three typical sources:

  1. A lost acknowledgement. The service processes the message, but the acknowledgement is lost on the way. The broker delivers it again.
  2. A client retry. The call times out and the client repeats it. It does not know whether the first attempt went through.
  3. Broker redelivery. After a connection or node failure, the broker delivers messages the consumer may already have seen.

The RabbitMQ documentation says so directly: messages can be redelivered and consumers must be prepared to handle deliveries they have seen in the past. It recommends designing consumers to be idempotent [1].

A broker guarantees “at least once”, not “exactly once”.

In our Event-Driven Architecture training we show this on a mortgage payout. The Disbursement service calls the payment system and the call times out. The money may have left, or it may not. Sending the payout again is a risk; not sending it is a risk too.

Diagram: the Disbursement service sends a payout of CZK 2,000,000 to the payment system, the response is lost and the service reports a timeout. The retry sends the same payout again. Result: sent twice. Text: A timeout is not a ‘no’. It is an ‘I don’t know’. A retry without idempotency pays twice.
A timeout is not a ‘no’. It is an ‘I don’t know’.

What happens without it

Take a service that opens a loan account once a mortgage is approved. It receives the message “open an account for application 42”, creates the account and acknowledges the processing.

The acknowledgement is lost. The broker delivers the message again. The service processes it as new and creates a second account.

There are usually two intuitive reactions, and both are wrong:

Reliable delivery and correct data do not go together without idempotency. In practice, “exactly once” is therefore made of two parts: at-least-once delivery plus idempotent processing.

How to ensure idempotency

Idempotency key

The client adds a unique identifier to every request. The server stores the result of the first processing against the key. When the same key arrives again, it does not perform the operation and returns the stored result.

Payments in Stripe work this way, for example. The server stores the status code and body of the first request made with a given key, and repeated requests with the same key receive the stored response. Keys can be removed once they are at least 24 hours old [2].

What matters is who creates the key: the client, before the first attempt. A key generated on the server would be different for every retry.

Inbox: recording processed messages

For messages from a broker, the key is the message ID. The consumer records in its own database which messages it has already processed and discards duplicates. The pattern is called Idempotent Consumer [3].

The condition is that the processing record and the data change are written in one transaction:

public async Task Handle(OpenMortgageAccount message)
{
    await using var tx = await db.BeginTransactionAsync();

    // Duplicate: the message has already been processed, do nothing more.
    if (await db.ProcessedMessages.AnyAsync(m => m.Id == message.MessageId))
        return;

    db.Accounts.Add(Account.Open(message.ApplicationId, message.Amount));
    db.ProcessedMessages.Add(new ProcessedMessage(message.MessageId));

    await db.SaveChangesAsync();   // both, or nothing
    await tx.CommitAsync();
}

The check and the write must be in one transaction, and the processed messages table needs a unique index on the ID. Otherwise two concurrent deliveries pass the check at the same time.

Inbox diagram: message ID 42 arrives twice. The service asks whether it has already processed ID 42. If not, it opens the account and records ID 42 in the database. If so, it discards the message and does nothing. Text: Inbox: same message, one processing. The processing record and the data change in one transaction.
The second delivery stops at the check of processed messages.

Naturally idempotent operations

Sometimes idempotency can be ensured without any record:

Where idempotency ends

An idempotent receiver protects only its own data. If the service calls another system while processing, for example to send an e-mail or a payment, it has to pass the idempotency key on. Otherwise the duplicate just moves one floor down.

On the sender side the Transactional Outbox pattern helps: the message is stored in the same database as the data change and sent only afterwards [4]. The Outbox ensures the message is not lost. The idempotent receiver is responsible for the duplicates that sending creates.

Idempotency in HTTP APIs

The HTTP specification defines idempotent methods: a repeated identical request has the same intended effect on the server as a single one. PUT, DELETE and safe methods such as GET are idempotent. POST is not [5].

That is why payments, orders and other POST requests use the Idempotency-Key header. The IETF is standardising it precisely so that non-idempotent methods can be retried safely [6].

In practice:

Typical mistakes

Five questions for your system

  1. What happens when this message arrives twice?
  2. Who creates the idempotency key, and does it stay the same on retry?
  3. Is the processing record written in the same transaction as the data?
  4. How long do we keep keys, and is it enough for the longest outage?
  5. Do we pass the key on to the systems we call?

Idempotency is not an optimisation. It is the condition for a retry to be safe.

You can try the duplicate yourself in our Event-Driven Architecture training, in the part on idempotency and in exercise 12, where the simulator shows the same scenario without and with an Inbox. The article Event Sourcing as the memory of a system builds on the same topic.

Get in touch


References and sources

Sources verified on 26 October 2026.

[1] RabbitMQ. Reliability Guide. RabbitMQ documentation. The section Data Safety on the Consumer Side: redelivery and the recommendation of idempotent consumers.

[2] Stripe. Idempotent requests. Stripe API Reference. Storing the response of the first request with a key, repeated requests and key retention.

[3] Richardson, Chris. Pattern: Idempotent Consumer. Microservices.io. Recording the IDs of processed messages in the database and discarding duplicates.

[4] Richardson, Chris. Pattern: Transactional Outbox. Microservices.io.

[5] Fielding, R.; Nottingham, M.; Reschke, J. RFC 9110: HTTP Semantics, section 9.2.2 Idempotent Methods. IETF, 2022.

[6] IETF HTTPAPI Working Group. The Idempotency-Key HTTP Header Field. Internet-Draft, IETF. A draft standard, not a final RFC.

Share this article

Next step

Would you like to set out on this path together?

We can start right with the situation you are dealing with now — from a concrete problem to your own design and its verification in practice.

Get in touch with Marek
Discuss your product