Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Idempotency Laravel Package

wendelladriel/laravel-idempotency

Add idempotency to Laravel routes with middleware that stores responses keyed by an idempotency header, preventing duplicate requests and making retries safe. Configurable storage and locking help protect payments, orders, and other critical operations.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require wendelladriel/laravel-idempotency
php artisan vendor:publish --tag="idempotency-config"

The published config/idempotency.php is the first file to read — it controls the header name (Idempotency-Key by default), the cache store used for storing responses, the TTL, the key prefix, and the fallback rules used to derive a key when the header is absent. Everything else in the package hangs off those five values.

Register the middleware

Laravel 11+ (bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'idempotency' => \WendellAdriel\LaravelIdempotency\Middleware\IdempotencyMiddleware::class,
    ]);
})

Laravel 10 and below: add the same alias to app/Http/Kernel.php $middlewareAliases.

First use case

Protect a single non-idempotent write endpoint — a payment, order, or webhook-receiver:

Route::post('/orders', StoreOrderController::class)->middleware('idempotency');

Then have the client send a stable key it generates once per logical operation (a UUID or ULID created before the first attempt, reused on every retry):

curl -X POST /orders -H "Idempotency-Key: 01HX9..." -d '{...}'

The first request executes; identical retries within the TTL get the stored response replayed instead of a second order. If you can see that header come back on a replay, you've wired it up correctly.

Implementation Patterns

Middleware on the route, key scoped by caller. The idempotency key is only unique within whatever fingerprint the package builds (method + path + request payload). If two tenants can send the same client-generated key, you want the key namespace to include the user: either a custom key generator in config, or a wrapper that prefixes auth()->id(). Otherwise tenant A can replay tenant B's response.

Route groups over per-route attributes. Idempotency is almost always a property of a class of endpoints (POST/PATCH/DELETE on /api/payments/*, /api/webhooks/*), so groups keep the intent visible:

Route::middleware('idempotency')->group(function () {
    Route::post('/payments', ...);
    Route::post('/refunds', ...);
});

Do not blanket-apply to api — GET and HEAD are already idempotent and you'll just pay cache overhead and muddy your cache keyspace.

Programmatic use for non-HTTP paths. The facade is the integration point when the "operation" isn't a single request — queued jobs, artisan commands, console-triggered syncs:

Idempotency::run($key, fn () => $this->charge($order));

This is the pattern for a webhook handler that enqueues work: let the HTTP layer dedupe the webhook delivery, and let Idempotency::run() inside the job dedupe the job retry. Two layers, two different keys, no double-charge.

Validate the key early. Rather than returning a 500 from the middleware when the header is missing, add the package's validation rule to the FormRequest so the failure is a normal 422 with a field name. This is the single biggest UX win for API consumers, because "you forgot Idempotency-Key" is otherwise indistinguishable from "your request was malformed."

Testing. Use the array cache store in phpunit.xml so tests don't need Redis, and assert on the replay marker header rather than trying to inspect cache internals:

$first = $this->withHeader('Idempotency-Key', 'k1')->postJson('/orders', $payload);
$second = $this->withHeader('Idempotency-Key', 'k1')->postJson('/orders', $payload);

$second->assertHeader('Idempotent-Replayed', 'true');
$this->assertSame(1, Order::count());

That second assertion — one row, not two — is what actually proves the middleware is in the chain. assertHeader alone will pass even if you accidentally excluded the route from the middleware and both calls just succeeded.

Production cache store. Point the package at the same Redis/Memcached instance your app uses for shared state, not array or file. Multiple app servers behind a load balancer each with a local file store means retries land on a cold node and execute twice — which is exactly the failure the package exists to prevent, now with a false sense of safety.

Gotchas and Tips

v1.5.1 changes the request fingerprint. Fallback input is now excluded from fingerprints. Two consequences:

  • If you rely on the config's fallback rules to derive keys from request input, keys derived from that path may now collide or match differently than in 1.5.0. Audit any endpoint where the client does not send the header.
  • Fingerprints are part of the cache key. During a rolling deploy, nodes on 1.5.0 and 1.5.1 compute different keys for the same logical request, so an in-flight retry can hit a node that doesn't see the original's stored response and execute successfully alongside it. For endpoints where double execution is expensive (charges, refunds, provisioning), deploy during a quiet window or drain the affected keyspace. This is a small, quiet release, but the fingerprint change is the kind that bites in the middle of a deploy.

Same key + different payload is a 409, not a replay. The package compares the stored fingerprint; a mismatch is treated as key misuse. That's correct behavior, but it means any nondeterministic field in your payload — a timestamp, a client-generated request_id, jittery array ordering — will turn legitimate retries into conflicts. Normalize the request before it reaches the middleware, or accept that retries break. This is the #1 source of "idempotency is flaky" reports.

Verify error-response behavior in your version rather than assuming. Whether validation failures and 5xx responses get cached and replayed materially changes your retry semantics: caching a 422 means the client can never fix its payload with the same key, and caching a 502 means you've persisted a transient failure. Check the middleware source for the status codes it stores before you rely on either behavior, and make sure clients that should retry after a transient error generate a fresh key.

Nothing large in the body. The full response is serialized into the cache. Streaming responses, file downloads, and endpoints returning large collections will bloat Redis and may exceed your store's value limits. Keep the middleware on JSON endpoints returning bounded payloads.

Middleware order matters. Place idempotency after authentication so the key namespace can include the user, and before anything with side effects (queues, mail, external calls) — the whole point is that the middleware short-circuits before the controller runs. Registering it globally with a low priority will look correct in tests and do nothing in production.

Key hygiene. Enforce a length cap and a UUID/ULID format in validation. A client that sends an auto-incrementing counter or a truncated string will collide across operations, and the failure mode is a user receiving someone else's stored response — a data-leak shape, not just a retry bug. Also confirm the TTL exceeds the longest client retry window you actually support; a 10-minute TTL with a 30-minute retry policy leaves a 20-minute hole where retries execute for real.

Extension points worth knowing. The key generator and the cache store are both config-driven closures — those cover most custom needs without touching the package. The Idempotent-Replayed response header is the cheapest observability you get: log/alert on it, and you'll immediately see which endpoints are retry-storms and which clients are implementing keys badly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource