Skip to content

Users Click My Submit Button Twice and Get Duplicates, How Do I Stop It?

6 min read

Your users click submit, nothing seems to happen, so they click again. Now you have two signups, two orders, or two charges for one action. The direct fix: disable the button the moment it is clicked and show a loading state, so the second click has nothing to press. Then, for the actions that really must not repeat, back it up on the server. Both halves matter, and a vibe-coded app usually has neither.

Why this happens

The button works. That is the problem. When a user clicks submit, your app sends a request and waits for the server to answer. On a fast laptop that takes a blink. On real phones and real networks it can take two or three seconds, and during those seconds your button looks exactly like it did before the click. No spinner, no colour change, nothing. A reasonable person assumes the click missed and clicks again.

An AI agent asked to "add a submit button that saves the order" builds a button that saves the order. It does not build the part where the button locks itself while the save is in flight, because you did not ask for that, and in the demo the agent clicks once on a fast connection and everything looks perfect. The gap only shows up when a real user on real hardware gets impatient.

How to check

You do not need to guess. Reproduce it:

  1. Open the form that creates something: a signup, an order, a booking.
  2. Throttle your network so it behaves like a phone. In Chrome DevTools open the Network tab and set the throttling dropdown to Slow 4G.
  3. Fill the form and click submit twice, quickly, the way an impatient user would.
  4. Check your database or dashboard. Two rows? Two emails sent? Two charges in Stripe? Then you are affected.

If clicking twice on a throttled connection produces one clean result and the button visibly locks after the first click, you are already fine.

The fix

  1. Disable the button on click and re-enable it only when the request finishes. Track a submitting state. While it is true, set the button to disabled and change its label to something like "Saving…" so the user can see the click landed.
  2. Wrap the whole thing in try/finally so the button re-enables even when the request fails. If you only re-enable on success, a failed request leaves the button stuck forever, which is a worse bug than the one you started with.
  3. Guard the action on the server too, for anything that must never happen twice. For payments, pass Stripe an idempotency key: a unique id per attempt, so a repeated request returns the original charge instead of making a new one. For database writes, add a unique constraint (for example on email, or on an order id the client generates once) so the second insert is rejected by the database, not just by the button.
  4. Re-run the throttled double-click test from the section above. One result, every time, even when you mash the button.

The trap to avoid

Disabling the button alone is not enough for money or anything irreversible. The button lives in the browser, and the browser cannot be trusted. A flaky connection can send the request twice on its own, and anyone can bypass your UI entirely. The visible loading state is for honest, impatient users, and it fixes the overwhelming majority of cases. The server-side guard, an idempotency key or a unique constraint, is for the ones that would cost you a refund and an apology. Do the button for good UX; do the server guard for anything you cannot undo.

Where this fits

Double submissions are invisible in every demo and obvious the first week real users arrive, usually as a confused email about being charged twice. It is exactly the kind of gap the free Readiness Report looks for: your own agent walks your real forms and flags the ones that fire twice under load. If you would rather have the fixes handed to you in order, button state first, server guards where they count, that is what the Finishing Pass is for. It pairs naturally with getting your failure states right, since a submit that can fail is a submit that can be double-clicked.