# Idempotency in APIs: Safely Repeating the Same Request

> Designing API operations that produce no side effects when repeated; idempotency keys and practical implementation at the application layer.

- Published: 2024-07-07
- Updated: 2026-09-07
- Category: Web Development
- Tags: API
- Reading time: 5 min read
- Source: https://muhammetsafak.com/blog/idempotency-in-apis-safely-repeating-the-same-request/
- Language: en-US
- Author: Muhammet Şafak

---
> **TL;DR:** An idempotent API operation has the same effect whether a request arrives once or many times. A client-generated idempotency key lets the server recognize retries and return the stored response, which makes POST retries safe.

Consider this scenario: a user initiates a payment, the request gets lost in transit, or the client receives a timeout. What should the client do? Should it retry? It has no idea whether the payment went through or not.

This ambiguity is an unavoidable property of systems that communicate over a network. The request was sent but no response arrived — that does not mean "nothing happened." Maybe the operation completed and the response just never made it back. Maybe it was interrupted halfway. Maybe it never started at all.

The solution is to accept this ambiguity and design retries to be safe. This concept is called **idempotency**.

## What is idempotency?

In mathematics, an operation is idempotent if applying it any number of times with the same input always produces the same result. In the context of APIs: sending the same request more than once should have exactly the same effect as sending it once.

The definition lives in HTTP's own specification. [In the words of RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-idempotent-methods), a method is idempotent "if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request"; of the methods the specification defines, PUT, DELETE and the safe request methods are idempotent. HTTP methods behave differently in this regard:

- **GET, HEAD, OPTIONS, PUT, DELETE** — idempotent by nature. Send the same `GET /users/5` request ten times; the result is the same.
- **POST** — not idempotent by nature. Each `POST /payments` can create a new payment.

The distinction matters in practice for a specific reason: the RFC says an idempotent request can be repeated automatically if the connection drops before the client reads the response, but adds that a client should *not* automatically retry a non-idempotent method. Making retries safe on `POST` is therefore our job.

The problem typically arises with `POST` and with operations that mutate state.

## The idempotency key

The core of the solution is this: the client generates a key for each unique operation and sends it along with the request. When the server sees this key, it asks: "Have I already processed a request with this key? If yes, return the same response; if no, perform the operation and store the result under this key."

If the client retries with the same key, the operation is not executed again — the previously generated response is returned instead.

```http
POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
  "amount": 9900,
  "currency": "TRY",
  "method": "card"
}
```

The best-known implementation of this pattern is Stripe. [Per its own documentation](https://docs.stripe.com/api/idempotent_requests), the client generates the key, Stripe suggests V4 UUIDs for it, and keys may be removed from the system once they are at least 24 hours old. You can apply the same pattern in your own APIs.

## Implementation in Laravel

The basic flow is: when a request arrives, first check whether a record for this key already exists in the cache (or database); if it does, return the stored response; if it does not, run the operation, store the response, and return it.

```php
// app/Http/Middleware/IdempotencyMiddleware.php
class IdempotencyMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $key = $request->header('Idempotency-Key');

        if (!$key) {
            return $next($request);
        }

        $cacheKey = 'idempotency:' . auth()->id() . ':' . $key;

        if ($cached = Cache::get($cacheKey)) {
            return response()->json(
                json_decode($cached['body'], true),
                $cached['status']
            );
        }

        $response = $next($request);

        // Cache everything below 5xx — 4xx included
        if ($response->getStatusCode() < 500) {
            Cache::put($cacheKey, [
                'body'   => $response->getContent(),
                'status' => $response->getStatusCode(),
            ], now()->addHours(24));
        }

        return $response;
    }
}
```

A few notes:

**Per-user keys.** I tie the key to the authenticated user via `auth()->id()`. This prevents collisions when different users happen to send the same key.

**Caching error responses.** In the code above, 5xx errors are not cached; the client should be able to retry. 4xx errors (client errors) are cached: if you resend the same malformed request, you get the same error back.

That is a deliberate choice, and it differs from Stripe's: [Stripe saves the status code and body of the first request for a given key regardless of whether it succeeded or failed](https://docs.stripe.com/api/idempotent_requests) — subsequent requests with the same key get back even `500` errors verbatim. Which one you want depends on your answer to a single question: should a retry be able to get past a server-side error? If you expect transient faults on the server, don't cache 5xx, as I do here; if you want a retry to be absolutely incapable of producing a new side effect, Stripe's behaviour is the safer one.

**Cache duration.** 24 hours is a reasonable starting point — Stripe likewise prunes keys once they are at least a day old. Adjust it based on your business requirements.

## Who generates the key?

The client should. If the server generated it, it would be meaningless: the server has already performed the operation and returned the response by the time the key would be generated. Stripe describes it the same way: the client generates the key and the server uses it to recognise subsequent retries of the same request. The client must have a UUID v4 or similar unique identifier on hand and must preserve that identifier across retries.

On the React Native side:

```tsx
import { randomUUID } from "expo-crypto";

async function createPayment(amount: number) {
  const idempotencyKey = randomUUID();

  try {
    const response = await api.post(
      "/payments",
      { amount },
      { headers: { "Idempotency-Key": idempotencyKey } }
    );
    return response.data;
  } catch (error) {
    if (isNetworkError(error)) {
      // Retry with the same key
      return retryWithSameKey(idempotencyKey, amount);
    }
    throw error;
  }
}
```

The key is generated when the payment object is created; the same key is reused on retries. When the server sees the second request, it already knows it has processed it.

## Not just payments

The value of idempotency is not limited to payment systems. User registration, email delivery, inventory reservation — any POST operation that has a user-visible side effect can benefit from this pattern.

Mobile applications in particular deal with unreliable network connectivity; users can tap a "Submit" button more than once. Disabling the button on the client side to prevent duplicate submissions is not enough on its own — the server needs to be prepared as well.

[Building your design around this assumption](/blog/contract-first-api-design/) — "a request may arrive more than once" — leads to a more resilient API.

---

### Sources

- [RFC 9110: HTTP Semantics — Idempotent Methods](https://www.rfc-editor.org/rfc/rfc9110.html#name-idempotent-methods)
- [Stripe API Reference: Idempotent requests](https://docs.stripe.com/api/idempotent_requests)

## Frequently asked

### What is idempotency in APIs?

Sending the same request more than once has exactly the same effect as sending it once. RFC 9110 calls an HTTP method idempotent if the intended effect on the server of multiple identical requests is the same as for a single one. GET, HEAD, OPTIONS, PUT and DELETE are idempotent by nature, while POST is not.

### Why does idempotency matter when a request times out?

Because the client cannot know whether the operation completed, was interrupted or never started. An idempotent request can be repeated automatically if the connection drops, but a client should not automatically retry a non-idempotent method such as POST. Making retries safe on POST is therefore our job.

### How does an idempotency key work?

The client generates a unique key for each operation and sends it with the request, for example in an Idempotency-Key header. If the server has already processed that key, it returns the stored response. If not, it performs the operation and stores the result under the key, so a retry never runs the operation again.

### Who generates the idempotency key?

The client. A server-generated key would be meaningless, because by the time it exists the server has already performed the operation and returned the response. The client uses a UUID v4 or a similar unique identifier and keeps the same identifier across retries.

### Should error responses be stored for a key?

That is a deliberate choice. In my Laravel middleware, 5xx responses are not stored so a retry can get past a server-side error, while 4xx responses are. Stripe saves the status code and body of the first request regardless of outcome, which is the safer behavior if a retry must be absolutely incapable of producing a new side effect.

### How long should keys be kept?

24 hours is a reasonable starting point, and Stripe likewise may remove keys once they are at least 24 hours old. Adjust it to your business requirements. I also tie the key to the authenticated user, so that different users who happen to send the same key do not collide.
