AK/Wri/Same Button Twice

Same button twice

2 min read

when i opened a wallet app on my phone, i tapped pay. the screen said processing and nothing moved for a few seconds. i tapped again because it felt stuck. later i saw two charges for the same order.

that was not me being careless. the server got two separate requests and treated both as new work.

why it happens

networks drop responses, not just requests. the client cannot tell if the server finished.

client POST /charge
server charges card (done)
response lost on the way back (timeout)
client retries POST /charge
server charges card again

a timeout does not mean the first request failed. the work may already be done.

idempotency

idempotent = same operation run twice leaves the system in the same state as running it once.

  • GET is safe to repeat. reading a profile twice does not create two profiles.
  • POST is not safe by default. each call can create a new side effect unless you guard it.

fix: idempotency-key

generate one key when the user presses the button. send the same key on every retry for that action.

client:

TYPESCRIPT
const key = crypto.randomUUID();

await fetch("/api/charge", {
  method: "POST",
  headers: { "Idempotency-Key": key },
  body: JSON.stringify({ amount: 499 }),
});

server:

TYPESCRIPT
const key = req.headers.get("Idempotency-Key");
const hit = await store.get(key);
if (hit) return hit;

const result = await chargeCard(body);
await store.set(key, result, { ttl: "24h" });
return result;

first request does the real work. later requests with the same key get the stored response. no second charge.

where it matters

  • payments and checkout flows
  • webhooks (stripe, cloudinary, github can deliver the same event twice)
  • create and publish endpoints
  • mobile clients on bad wifi (retries are normal, not rare)

places i keep forgetting to add it

webhook handlers on this site and anywhere else. if the endpoint was slow or returned 500, the provider will send the same payload again. without dedup you re-run side effects for every redelivery.

form submits that hang on submit. admin publish buttons. anything with a spinner where the user might click twice.

the rule i remember

if duplicate execution would hurt someone (money, inventory, emails, votes), make the operation idempotent or make the client unable to fire twice without a key.

one click should mean one outcome, even when the wire tries twice.