Preventing Duplicate Writes on Remix Action Retries
0 reputation · 24 Aug 2021, 13:31 UTC
0 reputation · 24 Aug 2021, 13:31 UTC
The goal is to guarantee that a single user intent results in at most one write to the database, even when network instability triggers automatic retries of the same Action request.
Remix’s Action functions are invoked via HTML Form submissions or the useSubmit hook, but the framework does not provide a built‑in idempotency key or request‑uniqueness tracking. A retry will send another POST to the same endpoint, potentially causing duplicate mutations unless external logic is added.
Which strategy can be adopted within Remix to detect and ignore repeated Action calls? How can a server‑side idempotency token be generated, stored, and validated without breaking the existing request flow? Is there a recommended pattern for persisting request identifiers that works cleanly with Remix’s data‑mutation lifecycle?
34775 reputation · 24 Aug 2021, 21:29 UTC
To guarantee that a single user intent results in only one database write, you must implement a Client-Generated Idempotency Key pattern. Since Remix does not provide built-in request deduplication, you must generate a unique identifier on the client, send it with the Action request, and validate it on the server before executing the mutation.
crypto.randomUUID()) when the form is first rendered or when the user initiates the action. Store this key in a hidden input field within the <Form> or pass it via useSubmit.action function, extract the key from the formData: const idempotencyKey = formData.get("idempotencyKey");.| Storage Method | Pros | Cons |
|---|---|---|
| Redis (SET NX) | Atomic, high performance, shared across server instances. | Requires additional infrastructure. |
| Session Cookie | No extra infra; tied to the user session. | Limited to ~4KB; can be cleared by the user. |
| DB Unique Constraint | Absolute data integrity. | Causes hard errors that must be caught/handled; doesn't prevent the Action logic from running. |
To verify this implementation, use a test Action that increments a counter. Submit the form and manually trigger a second request with the same key; the counter should only increment once. For production safety, ensure the key is namespaced by the User ID to prevent collisions across different accounts.
Diagnostic Question: Are you operating in a multi-server/distributed environment? If so, a session-cookie approach will fail unless you use a shared session store, making Redis the recommended path.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 25 Aug 2021, 00:01 UTC
After the client sends a UUID in a hidden field or header, the Remix action should immediately attempt to insert that key into a dedicated idempotency_tokens table with a UNIQUE constraint. Using INSERT … ON CONFLICT DO NOTHING guarantees the insert is atomic and will silently fail if the key already exists.
Wrap the token‑insert and the business write inside the same transaction. If the insert affects zero rows, roll back and return a cached success response; otherwise proceed with the write, then commit. This pattern prevents race conditions when retries hit concurrently and ensures a single mutation per user intent.
await db.transaction(async (trx) => {
const { rowCount } = await trx
.insert({ key })
.into('idempotency_tokens')
.onConflict('key')
.ignore();
if (rowCount === 0) return; // duplicate
await trx.insert(...).into('posts');
});