Skip to main content
Laravel, shipping fast.

Chapter 8

Idempotency and Safe Retries

Julian Beaujardin

A consumer sends POST /api/licenses. Your API calls the provider, the provider creates the license, and your API writes the response. Somewhere between your server and the consumer, the connection drops.

The consumer saw a timeout. From where it stands, there are two possibilities and no way to tell them apart: the request never arrived, or it arrived and the answer was lost. So it does the only reasonable thing. It tries again.

Now there are two licenses.

Nobody made a mistake. The consumer was right to retry, and your API did exactly what it was asked, twice. Chapter 7 gave every failure a shape. This chapter is about the failures that don’t have one, because the consumer never received a response at all, and about making “try again” a safe thing to say.

Which Requests Are Already Safe

HTTP defines some methods as idempotent: sending the request twice leaves the server in the same state as sending it once.

  • GET changes nothing. Retry freely.
  • PUT and PATCH with absolute values set a state. Setting a name to “Partner A” twice gives one consumer named Partner A.
  • DELETE removes a thing. Removing it twice leaves it removed.
  • POST creates. Twice creates two.

The methods only promise this. Your code has to keep the promise. Chapter 3 made the driver treat a 404 on delete as success, and that is what makes DELETE /api/licenses/{license} idempotent in fact and not just in name. An API that answers 404 to the second delete has turned a safe retry into an error the consumer has to interpret.

Watch for the exceptions. A PATCH that says “add 10 to the rate limit” is not idempotent. Design updates as “set to this value” whenever you can, and the method keeps its promise.

That leaves POST. It can’t be made idempotent by nature, so it has to be made idempotent by agreement.

The Idempotency Key

The agreement is simple. The consumer generates a unique value for each operation it wants to perform and sends it in a header:

POST /api/licenses
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324

If it has to retry, it sends the same value again. The server’s side of the bargain:

  1. The first time it sees a key, it does the work and remembers the response.
  2. Any later request with the same key gets the remembered response. The work is not done again.
  3. The same key with a different request body is an error, because it means the consumer has a bug.
  4. The same key arriving while the first is still running is told to wait.

The key identifies an operation, not a request. “Create the license for acme.test” has one key however many times it is sent.

The audio could not be loaded. Try again in a moment.