Charging twice on purpose

Week 2 — SLOP3092

← Back to the lecture page

Last week

A read timed out.

You retried it.

It worked.

Nothing bad happened

That is the problem.

The reflex got rewarded before anybody examined it.

This week: same reflex, different call

POST /charges   { amount: 4200 }

The timeout looks identical from the outside.

What does a timeout tell you?

Two worlds

QuestionThe request was lostThe response was lost
Server charged?noyes
What you sawtimeouttimeout

You cannot tell these apart. Ever.

So the reflex charges twice

One customer. One button. Two charges.

Nobody wrote a bug. The retry was the bug.

The fix

POST /charges
Idempotency-Key: 9f2c-a41e-...

The client picks a key. The server checks it before it commits.

What it guarantees

The second request lands on the same charge. It does not create a second one.

The retry still goes out. The key makes it a no-op.

What the server has to remember

Every key, for as long as a client might still retry with it.

  • window too short → a late retry double-charges anyway
  • window too long → you are storing keys forever

Both ends are wrong in a different direction, so somebody has to pick the window and write down why.

In the lab

  1. Build the naive retry against a stub that commits, then times out
  2. Produce the double charge on purpose
  3. Add the key
  4. Confirm the same retry now lands on the same charge

Bring the double charge to the first review.

Next week

You fixed what the retry does.

Now: how often, and how fast.

← Back to the lecture page