Skip to main content
Laravel, shipping fast.

Idempotency is half server and half client. The client half has to be written down, because a consumer who retries the wrong things, or with new keys each time, gets none of the benefit.

  • Generate one key per operation, before the first attempt, and reuse it for every retry of that operation.
  • Retry on: a network error or timeout, a 5xx, a 429 after the Retry-After delay, and a 409 whose code is request_in_progress.
  • Don’t retry on: a 4xx other than those. A 422 will be a 422 again.
  • Back off, with jitter. Double the wait between attempts and add a random part, so that a hundred clients that failed together don’t all return in the same second.
  • Stop. Give up after a fixed number of attempts and surface the failure.

Your own driver in Chapter 3 is a client of the provider, and stricter than this list: it retries only a 429, because it can’t send the provider an idempotency key and so can’t know that anything else is safe to repeat. That is the difference a key makes. With one, a consumer may retry almost anything. Without one, almost nothing.

Testing It

// tests/Feature/IdempotencyTest.php
it('creates one license for two requests', function () {
    Http::fake(['*/sites*' => Http::sequence()
        ->push(['data' => []])
        ->push(['data' => $this->license], 201),
    ]);

    $key = (string) Str::uuid();

    $send = fn () => $this->postJson(
        '/api/licenses',
        ['name' => 'Acme', 'domain' => 'acme.test'],
        ['Idempotency-Key' => $key],
    );

    $first = $send()->assertCreated();

    $send()->assertCreated()
        ->assertHeader('Idempotent-Replayed', 'true')
        ->assertExactJson($first->json());

    Http::assertSentCount(2);
});

$this->license is a provider payload. It is set in a beforeEach, which also authenticates a consumer that has a provider token and the licenses:write ability. Chapter 12 has a helper for that.

The assertion that matters is the last one. Two requests came in. The provider was called twice, once for the list and once to create the license, and both of those calls belong to the first request. The second request never reached the provider at all.

Write the other two as well: the same key with a different body gets a 422, and a request that failed leaves nothing behind, so the retry runs for real.

Chapter 8 Summary

  • A consumer that sees a timeout will retry, and it is right to. Design for it.
  • GET, PUT, and DELETE are idempotent only if your code keeps the promise. A second delete is a success.
  • For POST, accept an Idempotency-Key: do the work once, remember the response, replay it.
  • Scope the key to the consumer. Reject the same key with a different body. Answer 409 while the first is in flight.
  • Remember only responses that had an effect.
  • The key protects your API, not the provider behind it. Pass the key on, or converge on a natural key.
  • Document the client’s half: one key per operation, what to retry, backoff with jitter, and when to stop.

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