# 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.

**Author:** Matt King | **Published:** November 3, 2026 | **Category:** Security

---

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:

```blade
<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](/tools/cookie-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:

```blade
<meta name="csrf-token" content="{{ csrf_token() }}">
```

and send it:

```js
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:

```php
// 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](/free-scan), and see our guide to [fixing missing CSRF protection](/fix/missing-csrf-protection).

---

## 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.

