Preventing Duplicate Form Submissions
A double-clicked submit button, an automatic retry after a network timeout, a browser back-then-resubmit — any of these can make the server process the same form twice. A token mechanism is an effective defense against both CSRF attacks and duplicate submissions: when a form is served, the server generates a token, stores it in Redis, and passes it to the client as a hidden form field or URL parameter; on submission the token is validated and then deleted immediately.
Why This Problem Is Worth Solving
The business consequences of duplicate submissions are usually worse than they look: an order endpoint processed twice means two orders, and a payment request retried twice means a double charge. Disabling the submit button on the frontend only stops accidental clicks — it can't stop automatic retries at the network layer, let alone maliciously crafted requests. That's why this protection ultimately has to live on the server.
A CSRF attack works by tricking the user's browser into sending a request to the target site, and it succeeds because the browser automatically attaches cookies. The token, however, is not stored in a cookie, so a third-party site has no way to obtain it — which is why a single token mechanism can defend against both classes of problems at once.
How It Works
The token mechanism is a common way for web applications to prevent duplicate submissions and CSRF attacks. The basic idea: each time a form is served, the server generates a token and stores it in Redis, then passes it to the client as a hidden form field or URL parameter. When the client submits the form, it sends the token back along with it. The server checks that the token is valid while processing the form, and deletes the token once processing is done.
The two protection goals rely on different aspects of the mechanism. Duplicate-submission protection depends on "delete immediately after validation" — by the time the same token arrives a second time, it no longer exists and the request is rejected. CSRF protection depends on the token only being issued to legitimate pages — a request forged by an attacker simply doesn't contain it. Redis plays two roles here: centralized storage, so any instance in a multi-instance deployment can perform the validation; and expiry control, using TTLs to automatically clean up tokens that were never consumed.
Implementation Steps
- Generate a token on the server and store it in Redis. The token can be a random number, a UUID, or anything else — as long as it is sufficiently random and unique. Set an expiry while writing it:
# NX ensures an existing token is never overwritten; EX sets the expiry in seconds
# so stale tokens don't pile up
SET submit:token:<tokenValue> 1 EX 600 NX
-
Pass the token to the client. For a form submission, include it as a hidden field; for an AJAX request, send it as an HTTP header.
-
When the client submits the form, it sends the token back to the server. For a form submission, a bit of JavaScript in the submit handler can add the token as a hidden field; for an AJAX request, attach the token as an HTTP header before sending.
-
While processing the form, the server checks whether the token is valid. If it is, this is a legitimate request and processing can continue; if not, this is a duplicate submission or a CSRF attack and the request should be rejected.
-
After processing the form, the server deletes the token from Redis so it can't be reused.
The "validate" and "delete" in steps 4 and 5 must happen as a single atomic operation. If you do GET first and DEL second as two separate steps, two concurrent requests can both pass validation and duplicate-submission protection falls apart. A short Lua script lets Redis perform validate-and-delete atomically:
-- Validate and delete: if the token exists, delete it and return 1; otherwise return 0
-- The whole script runs atomically inside Redis, so only one concurrent request can succeed
if redis.call('GET', KEYS[1]) then
return redis.call('DEL', KEYS[1])
else
return 0
end
Security Considerations
The token mechanism is an effective defense against duplicate submissions and CSRF, but the token itself must be protected, or an attacker can turn it against you. Specifically:
The token must be sufficiently random and unique, or an attacker may guess it or reuse it.
The token must be protected in transit, or an attacker may intercept or tamper with it. Always use HTTPS, and never put the token in a URL that will end up in logs.
The token must have an expiry, or an attacker may reuse it.
The token must be protected while stored in Redis, or an attacker may steal or tamper with it. The Redis instance should not be exposed to the public internet, and authentication should be enabled.
Pitfalls and Caveats
Writing validation and deletion as two separate steps is the most common broken implementation. Under load testing, or when a user double-clicks quickly, both requests pass the GET check and each processes the business logic once. Use a Lua script or an equivalent mechanism to guarantee atomicity.
The expiry should be set based on how long users actually take to fill out the form. Too short, and the token expires while the user is still working through a long form, guaranteeing a failed submission; too long, and stale tokens pile up in Redis.
When validation fails, the frontend must give clear feedback and guide the user to refresh the page to obtain a new token before resubmitting — not silently drop the request. Otherwise users just see "nothing happened when I clicked" and keep hammering the button.
Also think through when to delete: whether to return the token when business processing fails, allowing the user to retry directly, depends on the idempotency requirements of the specific business — there is no one-size-fits-all answer.
Wrapping Up
The heart of the token mechanism is the "one-time credential": issued by the server, stored in Redis, validated on submission, deleted after use — one flow that solves both duplicate submissions and CSRF. When rolling it out, keep an eye on four things: token randomness, security in transit and at rest, a sensible expiry, and atomic validate-and-delete. Get those right and you have a low-cost, high-value protection you can apply broadly.
COMMENTS