Your idempotency key is only as good as the server honoring it

Automation Engineering · By Caleb Sakala · June 4, 2026

Cheerful cartoon courier holding up a giant golden key beside a shrugging armored gatekeeper at a keyless castle door

Search for idempotency key and every result tells you the same thing. Generate a unique string, attach it to a request header, and the server returns the saved response instead of running the operation a second time. Stripe's API reference, the MDN entry for the Idempotency-Key header, and the IETF draft that standardizes it all describe that one mechanism. Every one of them quietly assumes something an automation usually lacks: control over the server doing the work.

In an automation, a duplicate request almost never starts somewhere you own. It arrives from a webhook that fired a second time, a step that reran after a timeout, or two scheduled jobs that overlapped by a minute. The header trick does nothing for any of those unless the API on the receiving end already built support for it. Plenty of internal and third-party APIs never did.

What an idempotency key really protects

The Idempotency-Key header is a server-side contract. Stripe's own write-up on the pattern stores the key with the result of the first call, then returns that stored result for any repeat within a time window. Per Stripe's API reference, keys can run up to 255 characters and get cleared once they are at least 24 hours old. Brandur Leach's Postgres implementation goes further, splitting a charge into atomic phases with recovery points so a half-finished operation resumes safely instead of restarting from scratch.

All of that is good engineering, and all of it runs on the server. When you issue requests from an automation, you are the client, and your half of the contract is a single rule: send the same key when you retry. You collect the protection only if the receiver built the other ninety percent.

Where automations mint duplicate requests

Take a billing automation that calls a payment API once per invoice. The call succeeds on the remote end, the money moves, and then the network drops the response on the way back. Your step sees a timeout, marks the attempt failed, and reruns. The remote system already did the work, so a retry without a stable key charges the customer again.

Webhooks make this worse, because most providers promise at-least-once delivery, not exactly-once. A slow acknowledgement on your side, or a brief 500, and the same event lands again an hour later. Cron overlap is the quiet third case: a job scheduled every five minutes that occasionally takes seven will have two copies processing the same row.

The idempotency key fix lives on your side

Most people mint a fresh UUID for each attempt, and that backfires here, because a random key changes on every retry, so a server that does check sees a brand new operation each time and the duplicate sails through. The key has to stay stable across retries and unique per logical operation. Derive it from the business facts rather than the moment of sending. Charging invoice 8841 for the March run means the key is a hash of the invoice number and that period, identical no matter how many times the step fires.

Because the remote API cannot be trusted to deduplicate, keep your own ledger. Claim the key in a table with a unique constraint before the side effect runs. If the insert loses the race, someone already did the work and the step exits without calling anything.

import hashlib

def operation_key(invoice_id: str, period: str) -> str:
    raw = f"charge:{invoice_id}:{period}"
    return hashlib.sha256(raw.encode()).hexdigest()

def charge_once(conn, invoice_id, period, amount):
    key = operation_key(invoice_id, period)
    row = conn.execute(
        "INSERT INTO processed_ops (op_key) VALUES (%s) "
        "ON CONFLICT (op_key) DO NOTHING RETURNING id",
        (key,),
    ).fetchone()
    if row is None:
        return  # already charged for this invoice and period
    charge_provider(invoice_id, amount, idempotency_key=key)

The unique constraint is the real guard, and the header you send the provider is a bonus that pays off only when they honor it. Log every time the insert conflicts. A slow climb in those skipped-duplicate counts is an early signal that something upstream, a webhook source or an over-eager retry policy, is firing more often than you think, and it surfaces in your metrics long before a customer emails about a double charge.

Isolate the step that can hurt you

A procurement workflow that places supplier orders is the dangerous case, because a retried research or approval step should never be able to re-trigger an order worth thousands of dollars. The clean design keeps the order-placing call in one step that owns the dedup ledger, and nothing else can reach the vendor API. That maps onto how action-type routing works in Chase Agents: a research step emits a purchase_recommendation object that routes to an approval step, while only the orchestrator holds the tool that actually calls the supplier. A research step that reruns produces another recommendation and never a second purchase.

Better than an idempotency key: make the operation converge

Sometimes the key is unnecessary. An operation that sets a state is naturally idempotent, while an operation that applies a change is not. Updating invoice 8841 to status paid leaves the same row whether it runs once or five times, while subtracting fifty dollars from a balance does not. Where you can express a side effect as a state you are declaring, the duplicate problem disappears with no bookkeeping at all. Reach for that shape first, and save the ledger for operations that genuinely cannot be made convergent, like sending an email or posting a payment.

Consider a nightly sync that pushes 4,000 CRM contacts into a billing system. On its second run it will try to recreate every contact unless each write is keyed to a stable external ID. Wiring that sync on Chase Agents through a webhook trigger with an input schema that requires the external ID on every record forces the deduplication decision to the front, before a duplicate run can double-insert four thousand rows.

Open your most important automation and find the single step that spends money, sends a message, or writes to a system of record. Ask what happens if it runs twice inside the same minute. If the answer rests on a header you set and a vendor choosing to respect it, there is a bug waiting in that step that you have not hit yet. The retry that triggers it sits on the schedule already, and you do not know the date yet.