You hit "submit" on a form. The spinner turns. Nothing happens. You hit it again. And again.
If the backend isn't designed for that moment, you just created three submissions. Or three charges. Or three identical comments on someone's post.
The idea that prevents this is called idempotency, and it's one of those concepts that sounds academic until the day it saves your production database.
What idempotency actually means
An operation is idempotent when performing it once has the same effect as performing it many times. The first request does the work; every identical retry produces no new effect.
This matters because duplication pressure is normal behaviour, not an edge case. Users double-click and refresh. Networks time out mid-request, leaving the client unsure whether the server did the work. Load balancers, gateways, and retry policies in client libraries can all cause the same request to arrive more than once. If your API treats every arrival as a brand-new instruction, duplicates are a question of when, not if.
HTTP already gives you hints
The HTTP verbs encode this idea:
-
GET,HEAD,PUT, andDELETEare designed to be idempotent. Deleting a resource twice has the same end state as deleting it once: it's gone. -
POSTis not idempotent by convention. EachPOSTis, by default, a new "create" instruction.
That "by default" matters. A POST endpoint can absolutely be designed to behave idempotently. That's exactly what the idempotency-key pattern does, and it's why the duplicate-submission problem almost always gets solved on the POST endpoints: creating comments, processing payments, submitting assignments.
The idempotency-key pattern
The standard fix for non-idempotent endpoints is the idempotency key:
- The client generates a unique key for one logical operation (a UUID works fine).
- The client sends it with the request, usually as an
Idempotency-Keyheader. - The server records the key when it processes the request and treats later arrivals of the same key as duplicates of the first, skipping the side effects.
- Many implementations also store the first response and replay it to the retrying client, so a retried request gets the original answer back instead of an error. Storing and replaying the response is a design choice on top of the core guarantee, not a requirement for idempotency itself, but it makes retries behave the way users intuitively expect.
Change anything meaningful about the request and it becomes a new operation with a new key. Stripe popularised this pattern for payments, and it's the reason retrying a payment request doesn't charge a customer twice.
Implementation details worth thinking about:
- Enforce uniqueness with a database constraint, not application logic alone. Race conditions laugh at
if (!exists)checks. - Scope keys per user or tenant so one user's key can never mask another's request.
- Give keys a sensible expiry. You need them long enough to cover realistic retry windows, not forever.
Why this matters beyond payments
Once you see the pattern, you see the problem everywhere: webhook receivers, queue consumers (most queues guarantee at-least-once delivery, which means duplicates by design), sync engines, form submissions, file uploads.
We ran into the submission version of this while building Andika. A reply or an assignment that a user submits should exist exactly once, no matter how many times the request arrives. So requests carry a unique key, the first one is processed, and matching duplicates are recognised and ignored. It's a small amount of code that removes an entire category of bugs and support tickets.
For software engineering generally, idempotency is really about a mindset: assume every request may arrive more than once, and design the system so that's safe. Retries become harmless. Recovery becomes boring. Boring is good.
For machine learning pipelines the same principle shows up in data ingestion and training jobs: reprocessing a batch should not double-count it. Deterministic keys and dedupe at the write boundary keep datasets honest.
It's not glamorous. It's just correct.
This post was adapted, with AI assistance, from an article on the Andika blog, where we build a collaborative writing and knowledge platform.
Top comments (0)