Fix 'CSRF Token Mismatch' Form Errors
A "CSRF token mismatch" error means the secret code your form sent does not match the one your server expected, and the usual causes are an expired session, a cached page serving an old token, or a cookie the browser dropped.
This guide is for site owners, developers and agencies whose contact forms, checkouts or logins are failing with this error. You will get a step-by-step checklist, a quick-reference table of error messages, and platform-specific fixes for Laravel, Django, WordPress and Rails. You will also learn which "fixes" to avoid, because some of them quietly make your site less safe.
What a CSRF token is and why it fails
CSRF stands for cross-site request forgery. It is an attack where a bad website tricks a logged-in visitor's browser into sending a request to your site, such as changing an email address or submitting a form.
The standard defense is a CSRF token. Your server puts a random, hard-to-guess value into every form and ties it to the visitor, usually through their session (the server's memory of who they are) or a cookie. When the form comes back, the server checks the two values match. OWASP's CSRF Prevention Cheat Sheet describes this pattern in detail.
So the error is the protection doing its job. Your goal is to find out why a real visitor's token failed, not to switch the check off.
What the error looks like on each platform
| Platform | Message you see | Most likely cause |
|---|---|---|
| Laravel | "419 Page Expired" or "CSRF token mismatch" | Session expired, cookie not sent, or missing @csrf |
| Django | "CSRF verification failed. Request aborted." | Missing {% csrf_token %}, cached page, or untrusted origin |
| Rails | ActionController::InvalidAuthenticityToken | Cached form, token missing from a raw HTML form or JavaScript request, or session reset |
| WordPress | "The link you followed has expired" or "Are you sure you want to do this?" | Expired nonce (WordPress's version of a CSRF token), often from page caching |
| Custom or other | "Invalid CSRF token" | Any of the above |
Step 1: Reproduce it and inspect the request
Before changing anything, see what the browser actually sends.
- Open the failing page in a private window.
- Open DevTools (F12) and go to the Network tab. Tick "Preserve log".
- Submit the form and click the failed request (usually a POST).
- Under Payload or Form Data, look for a field named like
_token,csrfmiddlewaretokenorauthenticity_token. For AJAX calls, check the request headers forX-CSRF-TOKENorX-CSRFToken. - Under Cookies (or the Application tab), check that your session cookie exists.
This tells you which problem you have:
- No token in the request: the form or script is not including it (Step 5).
- Token present, cookie missing: a cookie or domain problem (Step 4).
- Both present but it still fails: the token is stale, so look at caching and sessions (Steps 2 and 3).
Step 2: Rule out session timeouts
In frameworks that store the token in the session (Laravel and Rails, for example), a token only works while that session is alive. If a visitor opens your form, goes to lunch, and submits an hour later, the session may be gone. The token no longer matches anything.
Django works differently. By default it keeps its CSRF secret in its own cookie, which lasts a year, so an expired login session does not by itself cause a Django mismatch.
Session timeouts are a frequent cause of "it works for me but not my customers" reports. Check these:
- Session lifetime. In Laravel,
SESSION_LIFETIMEin.envis in minutes (the default is 120). In Django, look atSESSION_COOKIE_AGE(seconds). In Rails, check yoursession_storeexpiry. - Long forms. Quote requests, applications and checkouts take time. Raise the lifetime for those pages, or save progress as the visitor goes.
- Multiple tabs. In some frameworks, logging in or out in one tab rotates the token and breaks forms open in another tab. Django does this on login, for example.
- Server restarts. If sessions live in temporary files or memory, a deploy or restart can wipe them.
A friendlier fix than a longer timeout is to catch the error and send the visitor back to the form with their input kept and a short message, instead of a dead-end error page.
Step 3: Check for caching conflicts
Caching is an easy-to-miss cause. If a cache stores the HTML of a page that contains a form, every visitor gets the same token. That token belongs to someone else's session, or to a session that has expired, so the check fails.
Check what your server tells caches to do:
curl -s -D - -o /dev/null https://example.com/contact | grep -i -E "cache-control|age:|cf-cache-status|x-cache|set-cookie"
This sends a normal GET request, which is what real visitors send. Some caches treat HEAD requests (what curl -I sends) differently, so they can give misleading results.
Red flags:
cf-cache-status: HITorx-cache: HITon a page with a form.- An
age:header with a large number. - A public
Cache-Controlsuch aspublic, max-age=3600on a page that holds a token.
Fixes, from best to acceptable:
- Exclude form pages from full-page caching. Do this in your CDN (content delivery network, the service that serves copies of your pages from nearby servers), caching plugin, or server config.
- Send
Cache-Control: private, no-storefor pages with forms or logged-in content. - Load the token with a small script after the page loads, from an endpoint that is never cached. This lets you cache the rest of the page.
In Django, wrapping a view in cache_page caches the token too. The Django CSRF docs cover how to handle this. Remember to purge your cache after you change the rules, or visitors keep getting the old copies.
Step 4: Fix cookie and domain problems
The token check usually depends on a cookie. If the browser does not send it back, the match fails.
- www vs non-www. A visitor loads
www.example.combut the form posts toexample.com. Cookies set on one are not always sent to the other. Pick one version and redirect the other to it. - HTTP vs HTTPS. If your session cookie is marked
Secure, the browser will not send it over plain HTTP. Redirect all HTTP to HTTPS. - Cookie domain setting. In Laravel, check
SESSION_DOMAIN(for example.example.comto cover subdomains) and setSESSION_SECURE_COOKIE=trueon HTTPS sites. - SameSite. The
SameSitecookie attribute controls when cookies are sent in cross-site situations. A form embedded in an iframe on another site needsSameSite=None; Secureto send its cookie. See the MDN reference on SameSite. Do not loosen this for ordinary forms on your own domain. - Cookie blocking. Strict privacy settings or in-app browsers can drop cookies. Test in a second browser and on a phone.
Our security headers tool checks your site's security-related response headers. To see the exact Set-Cookie flags (Secure, SameSite), use the browser DevTools Application tab (Cookies) as in Step 1.
Step 5: Find the missing token
If Step 1 showed no token in the request, the form or script is not adding one.
- Plain forms. Add
@csrf(Laravel) or{% csrf_token %}(Django) inside the<form>. Rails: useform_withorform_for, which add the authenticity token automatically. Raw HTML forms do not get a token automatically, so add one yourself, for example<%= hidden_field_tag :authenticity_token, form_authenticity_token %>in Rails. - JavaScript requests. Read the token from a meta tag or cookie and send it as a header. In Laravel, put
<meta name="csrf-token" content="{{ csrf_token() }}">in your layout and send it asX-CSRF-TOKEN. In Rails, put<%= csrf_meta_tags %>in your layout<head>so scripts can read the token. In Django, send theX-CSRFTokenheader. - Third-party form embeds. If a widget posts to your server from another domain, it cannot read your token. Use the vendor's own endpoint, or build a server-side handoff.
- Page builders and form plugins. Some copy form HTML into templates by hand. Re-create the form using the plugin's own block or shortcode.
Step 6: Check proxies, load balancers and multiple servers
If you run more than one server, a visitor's token may be created on server A and checked on server B. If sessions are stored in local files, B knows nothing about it.
- Store sessions in a shared place such as Redis or your database.
- Make sure your app knows it sits behind a proxy so it sees HTTPS correctly. In Django, set
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")only if your proxy sets that header reliably. In Laravel, configure trusted proxies. - In Django 4.0 and later,
CSRF_TRUSTED_ORIGINSmust include the scheme, for example["https://www.example.com"]. Entries without a scheme fail Django's startup system check (4_0.E001) instead of causing a runtime error. If you see "Origin checking failed" with "does not match any trusted origins", the real origin is missing from the list.
Quick fixes by platform
Laravel
php artisan config:clear
php artisan cache:clear
php artisan view:clear
Note: cache:clear can log out all users if your sessions are stored in the cache.
Then confirm @csrf is in the form and your session settings are correct. See the Laravel CSRF docs.
Django. Confirm django.middleware.csrf.CsrfViewMiddleware is enabled, the template has {% csrf_token %}, and CSRF_TRUSTED_ORIGINS is correct. For pages where JavaScript posts without a form, use the ensure_csrf_cookie decorator.
WordPress. WordPress uses nonces ("number used once"), which are valid for a limited time (24 hours by default, per the WordPress nonce docs). Set your caching plugin's cache lifespan well under 12 hours, or exclude pages with forms or login. Then clear the cache in WP Rocket, W3 Total Cache, LiteSpeed Cache or your host's built-in cache. If a form plugin such as Contact Form 7, WPForms or Gravity Forms sits on a cached page, exclude that page's URL from page caching, or turn on the plugin's "refresh nonce" or AJAX token option if it has one.
Rails. For JavaScript requests, make sure the layout <head> includes csrf_meta_tags, and avoid fragment-caching forms. The Rails security guide explains the defaults.
What not to do
The internet is full of advice that "fixes" the error by removing the check. Avoid it.
- Do not disable CSRF protection site-wide. It stops the error and removes the defense. A forged request can then change data using a visitor's login.
- Do not exempt cookie-authenticated routes. Token-authenticated APIs that do not rely on browser cookies are a different case.
- Only exempt webhook endpoints (for example from a payment provider) that verify their own signature instead. Keep that list short and review it.
- Do not paste an old token into a form or hard-code one. It defeats the point.
If you have already turned protection off to "get things working", turn it back on and use the steps above. A free website security check can show whether other basics are in place, and our guide on whether your website is hackable explains what an attacker looks at first.
Key takeaways
- A CSRF token mismatch means the token sent with the form did not match the one the server expected for that visitor.
- Look at the failing request in DevTools first. It tells you whether the token, the cookie, or both are missing.
- Frequent causes are expired sessions (in frameworks that store the token in the session, like Laravel and Rails), cached pages that serve a stale token, and cookie or domain mismatches (www vs non-www, HTTP vs HTTPS).
- Exclude form pages from full-page caching, and purge your cache after changing the rules.
- Never fix this error by turning CSRF protection off. Fix the cause instead.
Want a human to check the rest?
Fixing a form error gets your leads flowing again. It does not tell you whether the rest of your site is set up safely. A person can look at your forms, cookies, headers and logins together and write up what to fix. That is what a Bug Circuit manual audit does, for a one-time $49. It is a focused small-site audit, not a compliance certification. You can see what is included on the pricing page.
A real human security engineer audits your whole site by hand and sends a full report: every issue, its severity, and the exact fix. From $49, with a 14-day money-back guarantee.
See pricing