---
title: "Save each form only once · Agent Wranglers"
url: https://agentwranglers.se/playbook/app-ui/forms/single-submit
updated: 2026-10-20
licence: "All rights reserved"
---

# Save each form only once

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Forms](https://agentwranglers.se/playbook/app-ui/forms)
- Rule: Make a double click, a second Enter or a retry save the record once. Block repeat submits in the form and reject duplicates on the server.
- How: While a submit is in flight, ignore further submits and show that it is saving. Keep the button focusable. In React 19, read `pending` from `useFormStatus` or `isPending` from `useActionState`. Create one idempotency key when the form opens and send it with every attempt, as [idempotency keys](https://agentwranglers.se/playbook/api-design/reliability/idempotency-keys) describes. Make a new key only after a confirmed save, if the form stays open for another record. After a server-rendered post, redirect with 303, so a reload doesn't post again. In htmx, `hx-disabled-elt` and `hx-sync` block repeats.
- Check: A browser test clicks submit three times within 100 ms, and a second test sends the same request twice with one key. Each test creates exactly one record.

Example:

```ts
export function submitOnce(form: HTMLFormElement, send: (data: FormData, key: string) => Promise<void>) {
  const idempotencyKey = crypto.randomUUID();
  let isSending = false;
  form.addEventListener('submit', async event => {
    event.preventDefault();
    if (isSending) return;
    isSending = true;
    form.setAttribute('aria-busy', 'true');
    try { await send(new FormData(form), idempotencyKey); }
    finally { isSending = false; form.removeAttribute('aria-busy'); }
  });
}
```

```python
@app.post("/api/invoices")
def create_invoice_once():
    key = request.headers.get("Idempotency-Key")
    if not key:
        return problem(400, "idempotency-key-required")
    previous = idempotency.find(current_user().id, key)
    if previous:
        return previous.response
    return idempotency.store(current_user().id, key, invoices.create_from(request.json))
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/forms/single-submit`.

- Previous: [Never lose what people typed](https://agentwranglers.se/playbook/app-ui/forms/keep-user-input)
- Next: [Save drafts on their own](https://agentwranglers.se/playbook/app-ui/forms/autosave)
- [Forms](https://agentwranglers.se/playbook/app-ui/forms)
