Idempotency keys: where to put them and who generates them

The caller generates the key, the database enforces it with a unique index, and a repeat gets the original result rather than an error. All three, or you have a key that documents duplicates instead of preventing them.

Why the caller

A network timeout leaves the client not knowing whether the request arrived. Only the client knows that its retry is the same intent as the attempt that timed out. A server-generated key is generated twice and matches nothing.

Derive it from something stable in the caller's own domain - the order id and the event, say - rather than a random value regenerated on each attempt:

key = f"order-{order.id}-shipped"   # stable across retries
# not:
key = str(uuid4())                  # new every attempt, guards nothing

Why the database

A check-then-insert passes twice when two copies arrive at once, which is precisely the situation a retry creates. The unique index is the only component that can decide.

Store the response alongside the key, not just the key. On a repeat you must return what the first call returned - including the message id - or the caller cannot reconcile and will treat the duplicate as a new failure.

Scope and expiry

  • Scope to the tenant. Two customers using order-1-shipped must not collide.
  • Expire them. Twenty-four hours is usually plenty. Keeping keys forever means an order id reused next year silently returns a stale result.
  • Reject reuse with different content. Same key, different body is a bug in the caller and should say so rather than quietly returning the old message.

The failure it does not solve

Idempotency stops the same intent becoming two messages. It does nothing about a state machine that lets a delivered message slide back into sending and get picked up by a retry loop - that is a different bug with the same symptom, and it is covered in designing a send queue.

Questions people actually ask

Who should generate an idempotency key?

The caller. Only the caller knows that this attempt and the last one are the same intent - a server generating one has nothing to compare against.

What should a repeat request return?

The original result, with the same status. Returning a 409 makes a well-behaved client retry again, which is the opposite of what the key is for.

Njiwa

One inbox for WhatsApp, Facebook and Instagram, and an API that sends over either WhatsApp rail. 7 days free, no card.

Start a trial See pricing