Why Your API Needs Idempotency Keys (and How Frontend Retries Break Without Them)

11 Dec, 2025

8 min read

idempotency-cover

When building modern applications, especially those involving payments, bookings, or any operation that must not happen twice; one of the biggest challenges is ensuring that repeated requests don’t cause duplicate actions.

Network failures, user impatience (“let me click again!”), mobile dropouts, and browser refreshes all create scenarios where the same API request may be sent multiple times. If your backend isn't prepared, you’ll end up with double charges, duplicate orders, or corrupted data.


This is where idempotency comes in.


What Is Idempotency?

Idempotency means that performing the same operation multiple times results in the same outcome. That is, no side effects beyond the first execution.

Think of it like a doorbell: Tap once, tap ten times The bell rings. It doesn’t install a new bell each time.

In APIs:

  • GET /users/123 is naturally idempotent — you get the same user every time.

  • DELETE /orders/10 should be idempotent — delete is delete, even if the client calls it twice.

  • POST /payments is not idempotent by default — multiple POSTs might create multiple charges.

To make non-idempotent actions safe, we introduce Idempotency Keys.

What Are Idempotency Keys?

idempotency flow diagram

An idempotency key is a unique token generated by the client and included with an API request (usually in a header). The server stores the result of the first request associated with that key.

If the same key appears again, the server returns the original response instead of executing the action again. Example:

Post request with Idempotency Key

POST /checkout
Idempotency-Key: 7f1c21fa-f772-4ef5-9b5a-0fb83adb19b5

If the client retries the exact same request with the same key:

The server does not create another order.

The server instead returns the original result.

Why this matters:

Prevents double payments

Prevents duplicate database records

Makes retrying safe

Helps make APIs more reliable in mobile or unstable network environments


In the real world

Payments APIs like Stripe and Paystack use idempotency keys heavily for exactly these reasons.

How Idempotency Keys Work on the Backend (High-Level)

1
Client generates a key (usually a UUID).
2
Client sends the key with the request.
3
Server checks its idempotency store (a table or cache):
If the key exists → return stored response.
If not → perform operation.
4
Server stores:
request body hash
response payload
status code
expiry time (e.g., 24 hours)
5
Server returns response.

This ensures at-least-once delivery from the client but exactly-once execution on the server.

Idempotency key enforcement strategies

1. Strict Enforcement:

The server can make idempotency keys compulsory for some requests, by setting the idempotency_key request header as required, just like requiring an Authorization header.

app.post("/book-seat", async (req, res) => {
  const key = req.header("Idempotency-Key");
  if (!key) {
    return res.status(400).json({
      error: "Missing Idempotency-Key header"
    });
  }
  // proceed with idempotency logic...
});

This makes the API reject any write-operation that doesn’t include the key. This is the most common enforcement.

In the real world

Stripe requires an idempotency key for certain POST endpoints. If the client doesn’t send one → the request is rejected.

2. Optional Enforcement:

The API can accept requests without a key, but this configuration results in non-idempotent behavior.

In some APIs, idempotency is optional.

Clients that want safety include a key. Clients that don’t → take the risk.

This is typical in internal services where you fully control both client and server code.

What causes request retries?

Retries happen more often than even developers realize, and they can be caused by a number of reasons such as:

Network instability (mobile & Wi-Fi)

If the network drops during transmission:

  • browsers may retry POST requests (including `fetch`); when mobile networks switch between 4G/5G/Wi-Fi mid-request, TCP connections reset and the browser may retry the request without surfacing an error.

Browser Refresh During a Pending Request

If the user refreshes the page while a POST is still in-flight, the browser may:

cancel the original request

retry it automatically upon re-render

or your app logic might resend the action

Double-clicking (UI spam)

Users click:

  • "Pay"
  • "Book"
  • "Submit"

multiple times when the UI doesn't respond fast enough.

This is the most common cause of duplicate actions (charges, orders, bookings).

Client-Side Retry Logic

Many libraries include automatic retries:

React Query (default retry = 3 for failed requests)

Axios retry plugins

HTTP libraries on mobile (iOS/Android)

SWR

Apollo client (GraphQL)

Even devs can forget that these are enabled.

Handling Retries Safely on the Frontend

Even with backend idempotency, your frontend should also help reduce unnecessary repeated calls.

Use Idempotency Keys for Non-Idempotent Actions.

The frontend should generate a unique key before sending requests for:

Payments

Submissions

Checkout

Booking a seat

Creating a resource

Create Order post request with idempotency key

import { v4 as uuid } from "uuid";

async function createOrder(cart) {
  const key = uuid();
  const res = await fetch("/api/checkout", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: JSON.stringify(cart),
  });
  return res.json();
}
This should be accompanied with a UX check to make sure not to allow the user to click the submit button multiple times, which will then call this createOrder request and generate a new idempotency key. This defeats the purpose of having the key, as each request will be treated as a new request.

Disable UI Buttons During In-Flight Requests

Prevent “spam-clicking”

<button disabled={loading}>
  {loading ? "Processing..." : "Checkout"}
</button>

Detect Duplicate Submissions in the UI

If the user refreshes mid-checkout, store the idempotency key in:

localStorage, or

React Query’s cache

So if they retry, they reuse the same key.

Putting It All Together: A Safe Flow

On the Frontend

  1. 01
    Generate idempotency key
  2. 02
    Send request with key
  3. 03
    Store key until operation is complete or regenerate a new one if a variable changes (amount, currency etc).
  4. 04
    Retry using same key if:
    • - request fails
    • - page refreshes
    • - browser crashes
    • - network reconnects

On the Backend

  1. 01Check the idempotency key
  2. 02If seen before → return stored response
  3. 03If new → process and store the result
  4. 04Return consistent response on all retries

This combination ensures that no matter how shaky the network gets, your operation executes only once.

Final Thoughts

Idempotency is one of the most underrated concepts in API design. It turns unreliable networks into predictable systems and protects users from accidentally triggering expensive or irreversible operations multiple times.

If your app involves anything transactional—bookings, purchases, reservations—idempotency keys are not optional. They're essential.

On the frontend, combine idempotency keys with:

safe retry logic

button disabling

exponential backoff

local caching of request state

And you'll have a resilient, user-friendly system that "just works," even when the network doesn't.

Have an awesome project idea?

Feel free to reach out to me if you're looking for a developer, have a query, or simply want to connect.

Get in touch

Made by Emmanuella Okorie — Copyright 2026

twitter-iconlinkedin-icon github-icon