Idempotency in APIs: Why Retrying a Request Should Be Safe

Learn what idempotency means in backend development, which HTTP methods provide it, and how idempotency keys prevent duplicate operations.

Share on Linkedin Share on WhatsApp

Estimated reading time: 7 minutes

Article image Idempotency in APIs: Why Retrying a Request Should Be Safe

A user taps “Pay” on a checkout screen. The request reaches your server, the payment is processed, and then the connection drops before the response gets back. The app sees a timeout and retries. Does the customer get charged twice?

The answer depends entirely on whether your API is idempotent. This is one of those backend concepts that sounds abstract until it costs someone real money — and then it becomes the most important property your endpoints have.

What idempotency means

An operation is idempotent if performing it multiple times produces the same result as performing it once. The first call may change something; every identical call after that changes nothing further.

A light switch that toggles is not idempotent — pressing it twice returns you to the original state. A switch that sets the light to “on” is idempotent — no matter how many times you set it to on, the light ends up on.

Note that idempotency is about the final state of the system, not about the response being byte-for-byte identical. Deleting a resource that is already deleted may legitimately return a different status code the second time; what matters is that the resource is gone either way and nothing else was damaged.

HTTP methods and their guarantees

MethodSafe (no changes)IdempotentTypical use
GETYesYesRead a resource
HEADYesYesRead headers only
PUTNoYesReplace a resource entirely
DELETENoYesRemove a resource
POSTNoNoCreate a resource, trigger an action
PATCHNoNot guaranteedPartially update a resource

Two clarifications are worth making. First, PATCH is not idempotent by default. A patch that says “set status to approved” is idempotent; one that says “increment the counter by one” is not. The method does not decide this — your implementation does.

Second, these are specification guarantees, not automatic behaviour. If your PUT handler appends to a list instead of replacing it, the method is idempotent in name only. The contract is something you have to honour in code.

Why POST is the problem

POST exists precisely for operations that should happen once and produce something new each time: create an order, send an email, charge a card. That is exactly what makes retries dangerous.

And retries are not optional in distributed systems. Mobile networks drop. Load balancers time out. Client libraries retry automatically. Message queues redeliver. You cannot prevent a request from arriving twice — you can only decide what happens when it does.

Idempotency keys

The standard solution is an idempotency key: a unique identifier that the client generates and sends with the request, usually in a header. The flow works like this:

  1. Before sending, the client generates a unique value — typically a UUID — and attaches it to the request.
  2. The server checks whether it has already seen that key.
  3. If not, it processes the request, stores the key together with the resulting response, and replies normally.
  4. If it has seen the key, it skips processing entirely and returns the stored response.
  5. The client can retry as many times as it likes; only the first attempt has any effect.

The critical detail is that the client generates the key, not the server. If the server generated it, each retry would receive a new one and the deduplication would never trigger. The key must stay constant across all retries of the same logical operation.

Implementation details that matter

  • Store the key atomically with the work. If you record the key in one transaction and perform the operation in another, a crash between them leaves the system in an inconsistent state. Where possible, use a unique database constraint so a duplicate insert fails cleanly.
  • Handle concurrent duplicates. Two identical requests can arrive at the same moment on different servers. A unique index on the key, or a short-lived lock, prevents both from processing.
  • Cache the response, not just the key. The retrying client needs an answer. Returning an empty success is not equivalent to returning the order ID it expected.
  • Set a retention window. Keys do not need to live forever. Twenty-four hours is a common choice; anything longer bloats storage without practical benefit.
  • Reject key reuse with different payloads. If the same key arrives with a different request body, that is a client bug. Returning an error is safer than silently serving a mismatched cached response.

Idempotency beyond HTTP

The same reasoning applies wherever messages can be delivered more than once. Most message brokers guarantee “at least once” delivery, which means duplicates are expected rather than exceptional. Consumers therefore need to be idempotent too — usually by recording the identifier of each processed message and skipping repeats.

Database migrations, scheduled jobs and webhook handlers all benefit from the same discipline. A webhook provider that does not receive a success response will send the event again; if your handler creates a record every time, you will end up with duplicates in production.

A quick design checklist

  • Ask, for every endpoint that changes state: what happens if this runs twice?
  • Use PUT rather than POST when the client can determine the resource identifier.
  • Require idempotency keys on any operation involving money, messaging or external side effects.
  • Make retries explicit and bounded on the client, with backoff between attempts.
  • Write a test that sends the same request twice and asserts a single effect. It is the cheapest safeguard available.

Conclusion

Idempotency is what makes retries safe, and retries are unavoidable. Designing endpoints so that repeating them is harmless turns an entire class of race conditions and duplicate-charge bugs into non-events. It costs a little extra thought at design time and saves a great deal of investigation later.

If you want to go deeper into API design, error handling and distributed system patterns, the free backend development and programming courses on Cursa are a good place to continue.

Text Encoding Explained: ASCII, Unicode and Why You Sometimes See Strange Symbols

Learn how computers store text, what ASCII and Unicode actually are, why UTF-8 became the standard, and how to fix files that display garbled characters.

Idempotency in APIs: Why Retrying a Request Should Be Safe

Learn what idempotency means in backend development, which HTTP methods provide it, and how idempotency keys prevent duplicate operations.

What Is a CDN? How Content Delivery Networks Make Websites Fast

Learn what a CDN is, how edge caching and cache headers work, what a cache hit means, and when a CDN helps — or does not.

Semantic Versioning Explained: What a Number Like 2.4.1 Actually Tells You

MAJOR.MINOR.PATCH is a promise, not decoration. Learn to read version numbers and understand dependency range symbols.

What Is a Virtual Machine? Virtualization Explained for Beginners

Learn what a virtual machine is, how hypervisors work, how VMs differ from containers, and when to use each one.

How HTTPS Works: Certificates, the TLS Handshake and What the Padlock Really Means

A beginner-friendly walkthrough of HTTPS: what TLS certificates prove, how the handshake works, and what the browser padlock does not guarantee.

Big O Notation Explained: How to Talk About Code Efficiency

A beginner-friendly guide to Big O notation: what it measures, the most common complexity classes, and how to reason about the cost of your code.

What Is Docker? A Beginner’s Guide to Containers

Learn what Docker is, how containers work, and why this technology has become essential for modern software development and deployment.