On this page
You receive the same enquiry twice. A customer is charged twice. A sign-up list has the same person three times. Duplicate form submissions are annoying at best and expensive at worst — and they're almost always caused by ordinary user behaviour, not bugs.
Why duplicates happen
- Double-clicking the submit button (very common, especially among people used to double-clicking desktop icons).
- Impatience on a slow connection — nothing seems to happen, so they click again.
- Refreshing the results page after a POST, which asks the browser to resend the form.
- The back button followed by submitting again.
- Network retries by the browser, a proxy or your own JavaScript.
The fix is layered: stop most duplicates in the browser, and catch the rest on the server.
Layer 1: Disable the button while sending
The simplest, most effective fix:
const form = document.querySelector('form');
form.addEventListener('submit', function (event) {
if (form.dataset.submitting === 'true') {
event.preventDefault(); // already sending — ignore the second click
return;
}
form.dataset.submitting = 'true';
const button = form.querySelector('[type="submit"]');
button.disabled = true;
button.textContent = 'Sending…';
});
The flag matters as well as the disabled button: pressing Enter in a text field can submit a form even when the button is disabled.
If you submit with fetch(), re-enable the button when the request finishes — and keep it disabled on success:
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (form.dataset.submitting === 'true') return;
form.dataset.submitting = 'true';
button.disabled = true;
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
});
const result = await response.json();
if (result.success) {
form.reset();
showMessage('Thanks! We got your message.');
return; // leave the button disabled — the job is done
}
showMessage(result.message || 'Please try again.');
} catch {
showMessage('Network error. Please try again.');
}
// Only re-enable after a failure, so people can retry.
form.dataset.submitting = 'false';
button.disabled = false;
});
Show progress. A spinner or "Sending…" text stops most repeat clicks before they happen.
Layer 2: Post/Redirect/Get (PRG)
If a POST request returns a normal page, refreshing that page resubmits the form. The standard fix is Post/Redirect/Get: after handling the POST, the server responds with a redirect (HTTP 303) to a thank-you page. The browser loads that page with GET, so refreshing it is harmless.
With Formclick, you get PRG by adding a redirect field:
<input type="hidden" name="redirect" value="https://yourwebsite.com/thanks.html">
After submitting, the visitor lands on your thank-you page, and a refresh just reloads it.
Layer 3: Server-side duplicate detection
Browser fixes can't catch everything — JavaScript can fail, and bots don't use your button. The server should recognise repeats.
Content-based detection
Ignore a submission if it's identical to one received from the same source very recently. Formclick does this automatically: an identical submission to the same form from the same visitor within a couple of minutes is ignored, while the visitor still sees a normal success response. Combined with rate limiting, this also blunts simple flooding.
Idempotency keys (for payments and orders)
For actions where a duplicate costs money, use an idempotency key — a unique ID generated once per form load:
<input type="hidden" name="idempotency_key" value="">
<script>
document.querySelector('[name="idempotency_key"]').value = crypto.randomUUID();
</script>
On the server, store keys you've processed. If a request arrives with a key you've already seen, return the original result instead of doing the work again:
const existing = await db.get('SELECT result FROM processed WHERE key = ?', key);
if (existing) return existing.result; // duplicate: same answer, no new order
const result = await createOrder(data);
await db.run('INSERT INTO processed (key, result) VALUES (?, ?)', key, result);
return result;
Add a unique constraint on the key column so two simultaneous requests can't both slip through.
Natural uniqueness
Sometimes the data itself should be unique — one registration per email per event, one vote per person. Enforce it with a unique database constraint and show a friendly "You're already registered" message.
What not to do
- Don't rely only on a disabled button. It doesn't stop refreshes, Enter-key submits or bots.
- Don't silently drop retries after a failure. If the first attempt failed, the second must go through.
- Don't show an error for a detected duplicate — the person did nothing wrong. Show success.
Summary
| Cause | Fix |
|---|---|
| Double-click, impatience | Disable button + submitting flag + progress text |
| Refresh after submit | Post/Redirect/Get |
| Repeats that slip through | Server-side duplicate detection |
| Payments and orders | Idempotency keys + unique constraints |
Want the full pattern for JavaScript submissions? See How to Submit HTML Forms Using the Fetch API.



