Prevent Duplicate Laravel Form Submissions with Idempotency | FilaForms                                 [ ![Filaforms Logo](https://filaforms.app/logo.svg)FilaForms

 ](https://filaforms.app)  [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app/pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app)  [ Try Demo ](https://filaforms.app/login) [ Buy a license ](https://filaforms.app/pricing#plans) 

 [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app/pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app) [ Try Demo ](https://filaforms.app/login) [ Buy a license ](https://filaforms.app/pricing#plans) 

   ![FilaForms](https://filaforms.app/logo.svg) FilaForms 

 TutorialsPrevent Duplicate Laravel Form Submissions with Idempotency
===========================================================

 filaforms.app/blog

  [    Back to blog ](https://filaforms.app/blog) [ Tutorials ](https://filaforms.app/blog/category/tutorials) 

Prevent Duplicate Laravel Form Submissions with Idempotency
===========================================================

 Manuk Minasyan ·  October 13, 2026  · 4 min read 

  ![Prevent Duplicate Laravel Form Submissions with Idempotency](https://filaforms.app/storage/ink/01M327BH0GKZMKR8G5KE9G4GQB.webp)Disabling a submit button is good UX. It is not a correctness boundary. Two clicks can leave before the DOM updates, a proxy can retry after a timeout, and a queue worker can repeat a side effect after an unknown outcome.

Handle duplicates as four separate failures: double-clicks, client/network retries, concurrent requests, and retried downstream work.

Generate one key per logical attempt
------------------------------------

Create a cryptographically random UUID/ULID when the form renders or when a new logical attempt begins. Send it in an `Idempotency-Key` header or hidden field. Retrying the same attempt reuses the same key; intentionally starting again creates a new one.

Scope uniqueness to the tenant and form where appropriate. Save a canonical request hash with the key. If the same key arrives with a different payload, reject it as a conflict rather than returning an unrelated earlier result.

Define how long keys remain reusable. Do not recycle one while an original browser, proxy, or job can still retry it.

Make the database the arbiter
-----------------------------

An application check such as “does a matching row exist?” races:

```
Request A: no row found
Request B: no row found
Request A: insert
Request B: insert

```

Add a unique database index such as `(tenant_id, form_id, idempotency_key)`. Inside one transaction, claim the key, create the submission, and store the outcome.

On a uniqueness conflict:

- same key + same request hash + succeeded: return the original result;
- same key + different hash: return conflict;
- still processing: return documented pending/retry behavior;
- failed: follow an explicit retry or new-key policy.

Laravel documents [unique indexes](https://laravel.com/docs/13.x/migrations#creating-indexes) and [database transactions](https://laravel.com/docs/13.x/database#database-transactions). Cache locks can reduce wasted work but do not replace durable uniqueness.

Store an idempotency ledger
---------------------------

A separate record handles failures before the submission is complete:

```
scope + key (unique)
request_hash
state: processing | succeeded | failed
submission_id / response_status
expires_at

```

For a simpler system, those fields can live on `form_submissions`, but the separate ledger makes pending and failed attempts easier to represent. Avoid storing the full raw payload unless necessary; it expands privacy and retention obligations.

Dispatch side effects after commit
----------------------------------

Emails, CRM calls, capacity reservations, and webhooks should not run before the submission transaction commits. Otherwise a worker can observe missing data or send a message for a transaction that rolls back.

Dispatch after commit and give every logical side effect a stable event key. Laravel's `ShouldBeUnique` coordinates job dispatch through cache locks, but it is not a permanent business ledger; locks expire and unique constraints do not cover every queue pattern. Read the [unique jobs caveats](https://laravel.com/docs/13.x/queues#unique-jobs).

Make the receiver idempotent too
--------------------------------

An outbox delivery can time out after the destination applied it. Retrying is correct, but the receiver must recognize the stable event ID and avoid repeating the business effect.

This is at-least-once delivery plus idempotent handling—not a magical “exactly once” network guarantee. Payment providers use the same model; Stripe's [idempotent request behavior](https://docs.stripe.com/api/idempotent_requests) is a concrete industry example, not a universal protocol.

UX still matters
----------------

Use `wire:loading.attr="disabled"`, a clear pending state, and deterministic success navigation. These reduce accidental repeats and explain latency, while the server remains correct if JavaScript is bypassed. Livewire documents the [loading-state primitives](https://livewire.laravel.com/docs/loading).

Do not dedupe by answers alone
------------------------------

“Same email and payload within five minutes” can discard a legitimate second response. Formatting changes can also defeat a hash. Use a client-generated logical-attempt key or a business identifier such as a provider event/payment ID.

Capacity workflows need a separate invariant. Idempotency stops one request from being applied twice; it does not stop two different users from claiming the last seat unless a constraint or lock protects capacity.

Concurrency and failure tests
-----------------------------

- two simultaneous POSTs with one key create one submission and one side-effect set;
- retry after a committed timeout returns the original result;
- same key with changed payload is rejected;
- a rollback dispatches no notification/webhook;
- a retried job creates one downstream business effect;
- expired-key behavior is explicit;
- two keys with identical answers create two legitimate submissions;
- tenant/form scopes cannot collide.

Keep integrations off the visitor's critical path; the [form performance guide](/blog/form-performance-laravel-loading-validation-submission-speed) explains why. The next webhook guide applies the same stable-identity principle across the network.

Correctness comes from one durable uniqueness boundary and replayable outcomes. The disabled button is the friendly surface above it.

Stop rebuilding forms on every project.
---------------------------------------

 FilaForms gives your Laravel app a visual form builder, submissions, analytics, and notifications. One payment, self-hosted, no subscription.

 [ Buy from $99   ](https://filaforms.app/pricing#plans) [ Try Demo ](https://filaforms.app/login) 

 Related posts
-------------

 [  Tutorials   Oct 6, 2026  

 Secure Public Forms in Laravel: A Production Threat Model 
-----------------------------------------------------------

Threat-model Laravel public forms across validation, stored XSS, CSV export, file uploads, SSRF webhooks, authorization, availability, and tests.

 ![Secure Public Forms in Laravel: A Production Threat Model](https://filaforms.app/storage/ink/01M327BE4DNDJT3MK81NSN777M.webp) ](https://filaforms.app/blog/secure-public-forms-in-laravel-a-production-threat-model) [  Tutorials   Sep 29, 2026  

 Upgrading Public Forms from Filament 4 to Filament 5 and Livewire 4 
---------------------------------------------------------------------

Upgrade Filament 4 public forms to Filament 5 and Livewire 4 with compatibility gates, focused tests, endpoint checks, uploads, and safe rollout.

 ![Upgrading Public Forms from Filament 4 to Filament 5 and Livewire 4](https://filaforms.app/storage/ink/01M327BAVHPW6D8PWZXETQ704J.webp) ](https://filaforms.app/blog/upgrading-public-forms-from-filament-4-to-filament-5-and-livewire-4) [  Tutorials   Sep 22, 2026  

 Multi-Tenant Forms in Filament 5: Isolate Every Boundary 
----------------------------------------------------------

Secure multi-tenant Filament forms across public routes, Livewire, options, submissions, files, exports, caches, analytics, and queued jobs.

 ![Multi-Tenant Forms in Filament 5: Isolate Every Boundary](https://filaforms.app/storage/ink/01M327B7RFTWCVK0764HAGRZ4E.webp) ](https://filaforms.app/blog/multi-tenant-forms-in-filament-5-isolate-every-boundary) 

    ![FilaForms Logo](/logo.svg) FilaForms 

 Laravel form infrastructure for Filament. Stop rebuilding forms on every project.

 [ Buy a license   ](https://filaforms.app/pricing#plans) 

 ### Product

 [ Features ](https://filaforms.app#features) [ Documentation ](https://docs.filaforms.app) [ Blog ](https://filaforms.app/blog) [ Compare form builders ](https://filaforms.app/compare) [ Templates ](https://filaforms.app/templates) [ Pricing ](https://filaforms.app/pricing) [ About ](https://filaforms.app/about) [ Contact ](mailto:hello@filaforms.app) 

 ### Legal

 [ Terms of Service ](https://filaforms.app/terms-of-service) [ Privacy Policy ](https://filaforms.app/privacy-policy) 

  © 2025-2026 FilaForms. All rights reserved.

 [    ](mailto:hello@filaforms.app) [    ](https://x.com/MinasyanManuk)
