Skip to course content
Free generative AI app course

Generative AI Application Development with Python

Unit 06.02: Actions that must not run twice

A timeout leaves you unable to tell whether the action happened, and the retry is unavoidable.

A key derived from the request and the step

The same refund attempted three times with one key.

The code shows one refund issued.

ISSUED = {}


def issue_refund(account, amount, key):
    if key in ISSUED:
        return {"status": "already issued", "receipt": ISSUED[key]}
    ISSUED[key] = f"rcpt-{len(ISSUED) + 1}"
    return {"status": "issued", "receipt": ISSUED[key]}


key = "req-8841:refund"
for attempt in (1, 2, 3):
    print(f"attempt {attempt}: {issue_refund('ACC-1187', 120.0, key)}")
print(f"\nrefunds actually issued: {len(ISSUED)}")

# The key comes from the request and the step. A timeout leaves you unable to
# tell whether the call succeeded, so the retry is unavoidable -- and without
# the key it is a second refund.

The key is req-8841:refund - from the request and the step - so every retry of that step produces the same key. A key generated inside the call, from a timestamp or a uuid, makes each retry look like new work.

This is what makes the ambiguous timeout safe. You retry, and the second call is a no-op that returns the original receipt.

The mistake this prevents

The mistake is deduplicating on the arguments. Two genuine refunds of the same amount to the same account must both go through; two retries of one must not, and only a key tied to the request distinguishes them.

Takeaway

Derive the idempotency key from the request id and the step name. It is what makes the retry after an ambiguous timeout safe by construction.