How to stop duplicate form submissions
Short answer: Disable the submit button to stop double clicks, and send a stable Idempotency-Key header so a retried request returns the original result instead of creating a second submission. A refreshed thank-you page needs a redirect after POST, not another request.
Two identical leads in a dashboard look like a spam problem. They are usually three separate reliability problems wearing the same clothes.
Cause one: the double click
A slow response invites a second click. The fix is in the UI: disable the button when the submit handler starts and re-enable it only after a failure.
form.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button[type=submit]");
button.disabled = true;
try {
await send();
} finally {
button.disabled = false;
}
});Cause two: the retry after a timeout
A request can succeed on the server and still fail on the client, for example when the network drops before the response arrives. Retrying is correct behaviour, but without a key it creates a duplicate.
const idempotencyKey = crypto.randomUUID();
async function send(values) {
return fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey, // reused across retries
},
body: JSON.stringify(values),
});
}Cause three: the refresh
If a successful POST renders a page directly, pressing refresh re-sends the request. The standard answer is the same one the web has used for decades: redirect after a successful POST, so the browser's last request is a GET.
- Native form: set _redirect so the visitor lands on a plain page.
- JavaScript: replace the form with a success state instead of navigating.
- Framework: redirect from the action once it succeeds.
What deduplication cannot fix
Two genuinely different visitors with the same values are two leads. Formivo does not merge submissions by content, because that would silently discard real ones. It only guarantees that one retried request does not become two rows.
| Symptom | Cause | Fix |
|---|---|---|
| Two rows, same values, same second | Double click | Disable the button |
| Two rows minutes apart | Retry without a key | Idempotency-Key |
| New row on refresh | Rendered response | Redirect after POST |