Prevent Duplicate Charges with Idempotency Keys and 24 Hour Retention

Duplicate charges often start with a retry after a timeout, not a bad user action. This guide explains how idempotency keys let servers recognize the same payment request, store responses safely, and use a practical 24-hour retention window to reduce duplicate charges, order submissions, and other costly write errors.

Hubert Olkiewicz[email protected]
LinkedIn
6 min read

An idempotency key is a client-generated unique token that lets a server detect and ignore duplicate requests, so unsafe operations can be retried safely. The primary use case is retrying POST and PATCH calls after a network timeout or a 500 error, where the client genuinely does not know whether the original write succeeded. The practical recommendation: generate a secure UUID per logical operation, send it once, and pick a server-side retention window you document and enforce.


TL;DR:

  • Idempotency keys protect against duplicate operations mainly on non-idempotent POST and PATCH requests, reducing the risk of accidental repeated charges or orders.
  • Generate keys using secure UUID v4 or similar cryptographic randomness, ensuring one key per logical operation and storing it for retries, not regenerating on every attempt.
  • Servers must atomically claim and store deduplication records within the same transaction to prevent race conditions, using request fingerprinting to detect payload changes with the same key.
  • A typical retention window for stored idempotency records is around 24 hours, but developers must document their policy to avoid repeated retries exceeding the expiry and causing duplicate operations.
  • Use appropriate HTTP status codes, such as 409 for pending requests and 422 for payload mismatches, and handle retries with exponential backoff and respecting Retry-After headers.

Bitecode
Build Safer Payment Systems Faster
Bitecode helps organizations develop tailored financial software with ready-made components, automation, and scalable custom integrations.
Explore Bitecode

How Idempotency Keys Work: Client and Server Responsibilities

The Idempotency-Key header carries a client-generated identifier that the server uses to recognize a retried request and return the original result instead of executing the operation again, according to MDN’s header reference. The header itself does not enforce behavior. It is a contract, and the server defines exactly what “same operation” means within its own scope, whether that scope is per API key, per merchant account, or per endpoint.

Each side of that contract has distinct obligations:

  • Clients generate one unique key per logical operation, attach it to every retry of that same operation, and never reuse it for a different payload or a different intent.
  • Servers atomically claim a deduplication record the instant a key arrives, run the underlying business write only if the claim succeeds, persist the resulting response or resource pointer, and return that stored result verbatim to any duplicate that shows up later.

That last point is where most implementations quietly break. A server that checks for an existing key, finds none, and then runs the write in a separate step has built a race condition, not a safeguard. The claim and the write need to live inside the same transaction boundary, a pattern documented in detail by Digitarise’s writeup on safe retry semantics.

When Should You Use Idempotency Keys?

Idempotency keys exist to protect operations that are not naturally safe to repeat. Under HTTP semantics, that narrows the field considerably, per RFC 7231’s definitions of method semantics:

  1. POST and PATCH are non-idempotent by definition and are the primary targets. Creating a charge, submitting an order, or partially updating a record can each produce a different result (or a duplicate) on every call.
  2. GET, PUT, DELETE, and HEAD already carry idempotent semantics under HTTP itself, so an idempotency key is usually redundant there. Calling DELETE twice on the same resource just returns “already deleted” either way.
  3. High-stakes endpoints deserve the most scrutiny regardless of method: payment capture, account creation, irreversible workflow starts, and any call that triggers a third-party side effect like sending an email or dispatching a wire transfer.

Don’t apply idempotency keys everywhere reflexively. Pick the endpoints where a duplicate side effect actually costs something.

How Should You Generate and Format Idempotency Keys?

Generate keys with enough entropy that no two clients could plausibly collide, and keep them free of anything sensitive. Stripe recommends UUID v4 or an equivalent cryptographic random string, a practice grounded in RFC 4122’s UUID specification and readily available through standard libraries: Python’s uuid module, Node’s crypto.randomUUID(), or Ruby’s SecureRandom.uuid.

A few constraints matter more than they look:

  • Respect server-imposed length limits. Stripe caps keys at 255 characters, a sensible ceiling most implementations mirror, per Stripe’s idempotent requests documentation.
  • Never embed customer names, emails, account numbers, or any other identifying data inside the key string itself.
  • Send exactly one Idempotency-Key header per request, and reuse that exact value only when retrying the same logical operation, never for a new one.

Pro Tip: Generate the key once, at the moment the user action fires (button click, form submit), and store it in the client’s retry loop state. Generating a fresh key on every retry attempt defeats the entire mechanism.

Server-Side Patterns: Deduplication, Fingerprinting, and Expiry

A durable deduplication record needs a handful of fields to function correctly: scope (which resource or account the key applies to), the key itself, a fingerprint of the request, a state (PENDING or COMPLETED), a pointer to the stored response or resource, and timestamps for creation and expiry.

Deduplication record fields and expiry timeline

The claim has to be atomic. The server attempts an insert; if a row already exists for that key and scope, the insert fails, and the server reads the existing record instead of running the write again, a pattern detailed in Digitarise’s implementation guide. That insert and the business write belong inside one transaction boundary, not two sequential steps.

Fingerprinting closes a subtle gap. The server hashes a canonicalized version of the request body, commonly with SHA-256, and stores it alongside the key. The IETF Idempotency-Key draft recommends this specifically because a reused key with a different payload is far more dangerous than a duplicate: without a fingerprint check, the server would silently return a stale result for what the client thinks is a new operation, masking data loss rather than preventing it.

  • 400 when a required Idempotency-Key header is missing.
  • 409 when the original request with that key is still PENDING (a concurrent retry arrived mid-processing).
  • 422 when the key matches an existing record but the fingerprint does not, meaning the same key was reused with a different payload.

Retention is a genuine tradeoff between storage cost and safety margin. Stripe’s own retention window sits at roughly 24 hours, a figure documented in Stripe’s API reference, which balances typical retry patterns against the cost of storing every response indefinitely. Whatever window you pick, publish it. A client that retries after your keys have expired will trigger the operation twice, and that failure mode is entirely of your own making if the expiry was never documented. When appropriate, pair error responses with a Retry-After header so the client knows exactly how long to wait.

How Should Clients Handle Retries and Errors?

Retry logic only earns its keep when it follows a few firm rules:

  1. Retry only when the outcome is genuinely unknown, such as a timeout or a connection reset, and always reuse the same key for that retry.
  2. Apply exponential backoff with jitter between attempts rather than hammering the endpoint immediately.
  3. On a 409, back off and retry later; the original request is still processing.
  4. On a 422, stop retrying with that key. The payload changed, so generate a brand-new key for the new operation instead of forcing a mismatch.
  5. Honor Retry-After whenever the server sends it, per MDN’s Retry-After documentation, and log the idempotency key alongside every request attempt so support and engineering can trace a full retry chain later.

Security Considerations and Common Pitfalls

Low-entropy or predictable keys let an attacker enumerate deduplication records and pull stored responses that were never theirs to see, a risk the IETF draft flags directly. Validate key format server-side and never trust client input blindly.

  • Keep the claim and the business write inside one durable transaction; a non-atomic flow reopens the exact race condition idempotency was meant to close.
  • Store a payload digest rather than the raw request body when the payload contains anything sensitive, an approach worth pairing with broader PII data masking practices.
  • Restrict and audit who can read deduplication records; they can contain full response bodies.

Pro Tip: Treat your idempotency store like a mini financial ledger. If you wouldn’t log a field in plaintext elsewhere in your system, don’t let it sit unmasked in a deduplication record either.

Production Checklist Before You Ship Idempotency

Before an idempotency implementation goes live, work through this list:

  1. Document which endpoints require the Idempotency-Key header and which scope each key is validated against.
  2. Define your retention policy in writing, including how storage cost scales with request volume, and reference the tradeoffs Stripe’s own retention design illustrates.
  3. Confirm the fingerprint algorithm and the atomic claim transaction both survive concurrent-request tests, expired-key tests, and reuse-with-different-payload tests.
  4. Wire up monitoring for 409/422 rates, since a spike often signals a client bug rather than an attack.

Pro Tip: If your architecture already relies on an outbox pattern for eventing, tie idempotency claims to the same transaction that writes the outbox row. That keeps downstream consumers from double-processing an event that a duplicate API call already triggered upstream. Rate-limiting layers built on Redis and Lua scripting often sit right next to this logic, since both protect the same write path from abuse and duplication at once.

When Idempotency Keys Are the Right Tool

Idempotency keys shine on bounded commands where the client can mint one clean operation token. They are not a substitute for distributed transactions. Keep coverage narrow, favor durable operation IDs at each system boundary, and if retention cost gets prohibitive, a strict database unique constraint or workflow-level deduplication often does the job with less overhead.

— Bitecode

Get Help Building Idempotency Into Your Systems

Getting the atomic claim, fingerprinting, and retention policy right takes real engineering time, and getting any one of those wrong reopens the exact duplicate-charge or duplicate-order problem idempotency keys exist to prevent. Some software providers build custom enterprise systems from modular, pre-built components, so the deduplication pattern, observability, and error handling described above can be prepared in advance rather than starting from scratch.

Bitecode

Bitecode’s Financial Module and Automation Module are built to handle exactly this kind of hardened request handling, including downstream integration with eventing and workflow systems where duplicate side effects carry real financial cost. If your team is scoping a payment flow, an order pipeline, or any endpoint where a retry gone wrong means a duplicate charge, review Bitecode’s custom software services and start a conversation about what a production-ready implementation would look like for your architecture.

Sources

FAQ

What Is an Idempotency Key?

An idempotency key is a unique token the client generates and attaches to a request, letting the server recognize a retry and return the original result instead of repeating the operation. It is defined at the protocol level by the Idempotency-Key HTTP header.

Is Idempotency Good or Bad for API Design?

Idempotency is a safety mechanism, not a tradeoff with a downside when applied to the right endpoints. It adds a small amount of server-side complexity (a deduplication record and an atomic claim step) in exchange for making retries safe on payments, orders, and other non-idempotent writes.

How Do You Handle Idempotency in a REST API?

Require an Idempotency-Key header on sensitive POST and PATCH endpoints, atomically claim a deduplication record before running the write, and return the stored response for any duplicate. Reject a reused key with a different payload using a 422 response, following the pattern the IETF draft specifies.

Can You Give a Real-Life Example of Idempotency?

A payment API is the clearest case: a client submits a charge with a key, the network times out before the response arrives, and the client retries with the same key. Instead of charging the customer twice, the server recognizes the key and returns the original charge result, a behavior Stripe implements with a roughly 24 hour retention window.

Does Bitecode Build Idempotency Handling Into Custom Software?

Yes. Bitecode’s financial and automation modules are built with hardened request handling, including deduplication logic for payment and workflow endpoints, as part of its custom enterprise software projects. Pricing for custom implementation work is available on request through Bitecode’s site.

Articles

Dive deeper into the practical steps behind adopting innovation.

Software delivery6 min

From idea to tailor-made software for your business

A step-by-step look at the process of building custom software.

AI5 min

Hosting your own AI model inside the company

Running private AI models on your own infrastructure brings tighter data & cost control.

Hi!
Let's talk about your project.

this helps us tailor the scope of the offer

Przemyslaw Szerszeniewski's photo

Przemyslaw Szerszeniewski

Bitecode co-founder

LinkedIn