CSRF Token Mismatch and 419 Page Expired in Laravel: 10 Causes and Fixes
A 419 Page Expired or "CSRF token mismatch" error means Laravel could not match the token in the request to the one in the session. Here is how to find which of the ten usual causes you have, and fix it without turning CSRF protection off.
You submit a form and Laravel answers with 419 Page Expired. Or your JavaScript gets a JSON error: CSRF token mismatch. Both are the same thing. Laravel's CSRF middleware compared the token sent with the request to the token stored in the user's session, and they did not match, or one of them was missing.
The fix is almost never "turn CSRF off". It is finding which link in the chain broke: the token in the page, the cookie that carries the session, or the session itself. Work down this list.
How the check works
When a session starts, Laravel stores a random token in it. @csrf prints that token into a hidden _token field, csrf_token() returns it, and Laravel also sets an XSRF-TOKEN cookie with an encrypted copy for JavaScript clients. On every POST, PUT, PATCH or DELETE request, the VerifyCsrfToken middleware (ValidateCsrfToken from Laravel 11) reads the token from _token, the X-CSRF-TOKEN header or the X-XSRF-TOKEN header and compares it with the session copy. No session, or a different session, means no match.
So there are only three questions:
- Did the request carry a token?
- Did the request carry the session cookie, so Laravel loaded the same session?
- Is the token still in that session?
1. The form has no @csrf
The classic. Every HTML form that posts to a web route needs the field:
<form method="POST" action="/profile">
@csrf
@method('PUT')
...
</form>
View the page source and search for name="_token". If it is not there, that is your bug.
2. The session expired
SESSION_LIFETIME defaults to 120 minutes. A user who leaves a form open over lunch submits a token for a session that no longer exists, and gets 419. This is normal behaviour, not an attack. Options: raise the lifetime for that kind of page, refresh the token periodically on long-lived pages, or show a friendly message (see "Make 419 friendlier" below).
3. The session cookie never comes back
If the browser does not send the session cookie, Laravel starts a fresh, empty session on every request, and the token never matches. Open your browser dev tools, look at the request headers for the Cookie: line, and check your session cookie (named after APP_NAME, for example myapp_session) is there. Common reasons it is missing:
SESSION_SECURE_COOKIE=trueon plain HTTP. The browser refuses to send a Secure cookie overhttp://. Typical on a local or staging site without TLS.- Wrong
SESSION_DOMAIN. If it is set toexample.combut you browseapp.example.net, or it has a typo, the cookie is never stored. Leave itnullunless several subdomains share a session. SameSite=strictwith a cross-site flow. A POST arriving from another site (a payment provider redirect, an SSO form post) drops Strict cookies. Uselax, the default.- The app is embedded in an iframe on another site. Third-party cookies are blocked or partitioned in most browsers now. Avoid posting forms inside cross-site iframes.
You can see all of this at once by running your login page through our cookie security checker, which shows every cookie, its flags and its scope.
4. The session is not stored, or not shared
The cookie arrives but the session behind it is gone:
- File sessions and an unwritable directory.
storage/framework/sessionsmust be writable by the web server user. - Several servers with the file driver. Behind a load balancer each server has its own session files. Use
databaseorredisforSESSION_DRIVER. - Different
APP_KEYon different servers. Laravel encrypts cookies withAPP_KEY. If servers disagree, a cookie from one cannot be decrypted by another, and the user gets a new session. Every server must share the same key. - Redis evicting keys. If Redis runs out of memory with an eviction policy, sessions disappear early. Give sessions their own database or enough memory.
5. A cached page with a stale token
If a full-page cache or CDN caches HTML that contains _token, every visitor gets the same token, which matches nobody's session. Do not cache pages with forms for logged-in users, or load the token separately. Watch for this with Cloudflare cache rules that cache everything.
6. AJAX and fetch requests without the header
JavaScript requests need the token in a header. Put it in your layout:
<meta name="csrf-token" content="{{ csrf_token() }}">
and send it:
fetch('/comments', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
Axios does this for you on same-origin requests: it reads the XSRF-TOKEN cookie and sends it back as X-XSRF-TOKEN.
7. Sanctum SPA on another subdomain
For a separate front end (say app.example.com calling api.example.com) using Sanctum's cookie authentication, all of these must be true:
- the SPA calls
GET /sanctum/csrf-cookiebefore the first POST SANCTUM_STATEFUL_DOMAINSincludes the SPA's host (with port in development)SESSION_DOMAIN=.example.comso both hosts share the cookieconfig/cors.phphassupports_credentials => trueand names the SPA origin- the client sends credentials: Axios
withCredentials: true, and since Axios 1.6 alsowithXSRFToken: true, because newer Axios versions only attach the XSRF header to same-origin requests by default
8. The token was regenerated
Laravel regenerates the CSRF token at login and logout. A form opened before logging in (or in another tab before logging out) carries the old token. That 419 is correct; reload the page.
9. The request body was too big
If a POST is larger than PHP's post_max_size, PHP throws the whole body away, including _token, and Laravel sees a request with no token. Large file uploads that fail with 419 instead of a validation error usually mean this. Raise post_max_size and upload_max_filesize (and nginx client_max_body_size), and validate file size in the app.
10. A webhook or external system posts to a web route
Stripe, GitHub and other services cannot know your CSRF token. Exclude only those routes, and verify their signatures instead. In Laravel 11 and later:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/webhook',
]);
})
In older apps, add the path to $except in app/Http/Middleware/VerifyCsrfToken.php. Better still, put webhooks in routes/api.php, which has no CSRF middleware.
Make 419 friendlier
Users who hit an expired session see a bare "Page Expired". Publish a custom view at resources/views/errors/419.blade.php that explains the page timed out and links back, and keep the user's input where you can. Livewire shows its own "This page has expired" dialog, which you can also customise.
What not to do
Do not remove VerifyCsrfToken from the middleware stack or add * to the exceptions. CSRF protection is what stops another website from making a logged-in user's browser change their email, password or payout details. If you want to know whether a live app has CSRF protection switched off on its forms, run a free StackShield scan, and see our guide to fixing missing CSRF protection.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue, and most teams do not find out until something breaks. The free scan checks your live app in 60 seconds. Then StackShield re-runs every check after each deploy, so a fix you ship today does not quietly regress next week.
Frequently Asked Questions
What does 419 Page Expired mean in Laravel?
Laravel returns 419 when the CSRF token sent with a POST, PUT, PATCH or DELETE request is missing or does not match the token stored in the user's session. The usual causes are a missing @csrf field, an expired session, or a session cookie that never reached the server.
How do I fix "CSRF token mismatch" on an AJAX request?
Send the token in a header. Add <meta name="csrf-token" content="{{ csrf_token() }}"> to your layout and send it as X-CSRF-TOKEN, or let Axios send the XSRF-TOKEN cookie back as X-XSRF-TOKEN automatically on same-origin requests.
Should I disable CSRF protection to fix 419 errors?
No. Find the cause instead. Only exclude routes that genuinely receive requests from other systems, such as payment webhooks, and verify those with a signature.
Why do I get 419 after the page has been open for a while?
The session expired. Laravel's default SESSION_LIFETIME is 120 minutes; after that the stored token is gone and the form's token no longer matches. Refresh the page, raise the lifetime, or refresh the token in long-lived pages.
Why does my Sanctum SPA get CSRF token mismatch?
The SPA must call /sanctum/csrf-cookie first, its domain must be listed in SANCTUM_STATEFUL_DOMAINS, SESSION_DOMAIN must cover both hosts, and the HTTP client must send credentials. Axios 1.6 and later also needs withXSRFToken: true for cross-origin requests.
Related Articles
StackShield Now Has an MCP Server: Run Scans and Triage Issues From ChatGPT, Claude and Cursor
Connect StackShield to any MCP-compatible assistant and ask it to scan a domain, explain a failed test, resolve the issues you have fixed, or write a security report. Included on every plan.
SecurityComposer Says Packages "Were Not Loaded Because They Are Affected by Security Advisories": What to Do
Since Composer 2.9, composer update refuses to install package versions with known security advisories. Here is what the error means, how to fix it properly, and how to ignore a specific advisory with a recorded reason when you genuinely are not affected.
SecurityLaravel 12 vs 13: What Actually Changed, and When You Need to Upgrade
Laravel 13 is a small upgrade with a few changes that matter: PHP 8.3, origin-aware CSRF protection, hardened cache unserialization and JSON session serialization. Here is the side-by-side, the support dates, and the upgrade items that can log your users out.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.