Security 9 min read Markdown

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.

Matt King
Matt King
November 3, 2026
Last updated: November 3, 2026
CSRF Token Mismatch and 419 Page Expired in Laravel: 10 Causes and Fixes

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:

  1. Did the request carry a token?
  2. Did the request carry the session cookie, so Laravel loaded the same session?
  3. 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=true on plain HTTP. The browser refuses to send a Secure cookie over http://. Typical on a local or staging site without TLS.
  • Wrong SESSION_DOMAIN. If it is set to example.com but you browse app.example.net, or it has a typo, the cookie is never stored. Leave it null unless several subdomains share a session.
  • SameSite=strict with a cross-site flow. A POST arriving from another site (a payment provider redirect, an SSO form post) drops Strict cookies. Use lax, 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/sessions must 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 database or redis for SESSION_DRIVER.
  • Different APP_KEY on different servers. Laravel encrypts cookies with APP_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-cookie before the first POST
  • SANCTUM_STATEFUL_DOMAINS includes the SPA's host (with port in development)
  • SESSION_DOMAIN=.example.com so both hosts share the cookie
  • config/cors.php has supports_credentials => true and names the SPA origin
  • the client sends credentials: Axios withCredentials: true, and since Axios 1.6 also withXSRFToken: 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.

Free security check

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.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup for the scan. Continuous monitoring on a 14-day trial, no card.

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.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.