What Is Idempotency?

An operation is idempotent if performing it many times produces the same result as performing it once. Sending the same request twice leaves the system in the same state as sending it once. Idempotency is what makes it safe to retry a request when a network failure has left the outcome uncertain.

What does idempotency mean?

The term is borrowed from mathematics, where a function is idempotent if applying it to its own output changes nothing: f(f(x)) = f(x). Taking an absolute value is idempotent — abs(abs(x)) always equals abs(x). Adding one is not: every application moves the result further along.

In software the same idea applies to state. Consider two ways of updating an account balance:

  • SET balance = 100 is idempotent. Run it once or fifty times and the balance is 100.
  • balance = balance + 100 is not. Every repetition changes the result.

A useful everyday comparison is a lift call button against a light switch on a toggle. Pressing the call button ten times summons the lift exactly as pressing it once does — the button is idempotent. Flipping a toggle ten times leaves the light in a different state than flipping it once.

Idempotent vs idempotency

The two words describe the same concept from different angles. Idempotent is the adjective applied to an operation, endpoint or function. Idempotency — also written idempotence — is the property that operation has. An idempotent endpoint exhibits idempotency.

Why does idempotency matter?

Idempotency exists to solve one specific problem: the client cannot tell the difference between a request that never arrived and a request that succeeded but whose response was lost.

When a connection drops mid-request, the calling application knows only that it did not receive a response. The server may have done nothing. It may equally have charged a card, dispatched an order or written a row, and then failed to deliver the confirmation. From the client's position these two outcomes look identical.

The client's only reasonable action is to retry. If the operation is idempotent, retrying is safe — the second request either does nothing or returns the original result. If it is not, retrying charges the customer twice.

The same problem appears wherever delivery is guaranteed at least once rather than exactly once. Message queues redeliver on consumer failure. Job schedulers rerun tasks after a crash. Service meshes retry automatically on timeout. In each case the infrastructure assumes your handler can absorb a duplicate, and idempotency is the property that lets it.

Which HTTP methods are idempotent?

HTTP semantics, defined in RFC 9110, specify which request methods are expected to be idempotent:

MethodIdempotentSafe
GETYesYes
HEADYesYes
OPTIONSYesYes
PUTYesNo
DELETEYesNo
POSTNoNo
PATCHNoNo

Safe and idempotent are often confused. A safe method does not modify state at all. An idempotent method may modify state, but repeating it causes no further change. PUT and DELETE are idempotent without being safe.

One caveat matters more than the table does: these are specification expectations, not guarantees about your code. A PUT handler that appends a row to an audit table on every call is not idempotent, whatever the RFC says. Idempotency is a property you implement, not one the method name confers.

What is an idempotency key?

POST is not idempotent by definition, yet it is what most APIs use to create resources and take payments — precisely the operations where duplicates cause the most damage. An idempotency key is the standard way to close that gap.

The client generates a unique value for each logical operation and sends it with the request, conventionally in an Idempotency-Key header. The server records the key alongside the result. If a request arrives carrying a key it has already seen, the server skips the work and returns the stored response.

The key must be generated by the client, must be unique per operation, and must not be reused for a request with a different payload. A UUID is the usual choice. One design point is easy to miss: Stripe stores the status code and body of the first request whether it succeeded or failed, so a retry returns the original error rather than silently trying again. Stripe popularised the pattern, and it has since been described in an IETF Internet-Draft, The Idempotency-Key HTTP Header Field. That draft remains unratified after several years, so treat the header as a widely adopted convention rather than a formal standard.

Some implementations also store a fingerprint of the request body next to the key. If the same key arrives with a different payload, the server can reject it outright rather than silently returning a response belonging to a different request.

How do you implement idempotency?

Implementing idempotency keys raises three practical problems.

Where the keys live. The store needs three things: fast lookup on every request, an atomic insert that cannot be won twice, and automatic expiry. That describes an in-memory key-value store with per-entry TTL.

Concurrency. Two duplicate requests can arrive simultaneously. Both check for the key, both find nothing, and both proceed. A read followed by a write is a race, not a check — the claim must be atomic, or guarded by a distributed lock.

How long to retain keys. Too short and the duplicate window reopens before the client has finished retrying. Too long and the store grows without bound. Twenty-four hours is a common default, and is what Stripe uses.

Storing idempotency keys in Java

Using Redisson, a single atomic call both checks for the key and claims it:

RBucket<String> claim = redisson.getBucket("idempotency:" + idempotencyKey);

// setIfAbsent is atomic. It returns false if the key already exists,
// so two concurrent duplicates can never both pass this check.
if (!claim.setIfAbsent(IN_PROGRESS, Duration.ofHours(24))) {
    String stored = claim.get();
    if (IN_PROGRESS.equals(stored)) {
        throw new RequestInFlightException(idempotencyKey);   // answer 409
    }
    return deserialize(stored);          // duplicate: return the original result
}

PaymentResult result = charge(request);
claim.set(serialize(result), Duration.ofHours(24));   // replace the marker
return result;

Because the check and the write happen in one operation, there is no window between them for a second request to slip through. To keep the key and the response together with a per-entry expiry, RMapCacheNative stores both in a single structure and uses native server commands for expiration rather than a scheduled eviction task. It requires Redis 7.4 or later, with some operations needing Redis 8.0 or Valkey 9.0, so check your server version before reaching for it.

Handling concurrent duplicates

When the work spans several steps that must not interleave — writing to a database, publishing an event, then recording the result — an atomic claim is not enough on its own. A lock keyed on the idempotency key serialises the whole operation:

// tryLock is interruptible, so the enclosing method declares it
public PaymentResult handle(Request request, String idempotencyKey)
        throws InterruptedException {

    RLock lock = redisson.getLock("idempotency:lock:" + idempotencyKey);

    // wait up to 5s to acquire; release automatically after 30s
    if (!lock.tryLock(5, 30, TimeUnit.SECONDS)) {
        throw new ConcurrentRequestException(idempotencyKey);
    }
    try {
        return processExactlyOnce(request);
    } finally {
        // the lease can expire while the work runs, and unlock()
        // throws if this thread no longer holds the lock
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

The lease time matters. If the holder dies mid-operation the lock must expire, or a crashed request blocks every retry of itself. See using locks in Java for the full pattern.

One limitation is worth stating plainly. Keys held in an in-memory store are not durable by default. If a duplicate charge would be unacceptable even after total cluster loss, the idempotency key needs to be written inside the same transactional boundary as the business record it protects. In-memory storage buys speed and automatic expiry; it does not by itself buy durability.

Common idempotency mistakes

  • Deriving the key from the request body. Two legitimately identical requests — the same customer buying the same item twice — get collapsed into one. The key must be supplied by the client.
  • Checking and then writing. A non-atomic read followed by a write is a race that duplicates will eventually win.
  • Omitting a TTL. Keys accumulate indefinitely and the store grows until it is evicting live data.
  • Storing the key but not the response. Retries are correctly suppressed, but the client receives nothing useful and cannot tell success from failure.
  • Ignoring the in-progress state. A retry arriving while the original is still running needs a defined answer, usually a 409 telling the client to wait.
  • Assuming PUT is idempotent because the specification says so. It is idempotent only if your handler makes it so.

Similar terms

Redisson provides the distributed objects that idempotency depends on — atomic buckets, locks, TTL-backed maps and rate limiters — through one API across Valkey and Redis. Start a free trial.