# 419 Page Expired (Laravel CSRF Token Mismatch)

> Laravel 419 Page Expired means the CSRF token did not match the session. Causes: expired session, missing @csrf, dropped cookie, cached forms. Fixes included.

Source: https://howhttpworks.com/status-codes/419
Last reviewed: 2026-10-04

> **TL;DR:** 419 is Laravel's own status for a CSRF token mismatch: the token in the request did not match the one in the session. Check that the form has `@csrf`, that the session did not expire while the page was open, and that the session cookie is actually being stored and sent back.

## What it means

419 is not an HTTP standard code. Laravel's CSRF middleware throws a `TokenMismatchException` when the token submitted with a POST, PUT, PATCH or DELETE request is missing or does not equal the token in the user's session, and the framework renders it as 419 with the title "Page Expired". Browsers show the default page, and AJAX clients that send `Accept: application/json` get a JSON body:

```http
HTTP/1.1 419 unknown status
Content-Type: application/json

{"message": "CSRF token mismatch."}
```

The status line often reads `unknown status` because Laravel's underlying Symfony response class has no reason phrase for 419. That is cosmetic; the code is what matters.

The token travels as a hidden `_token` form field, an `X-CSRF-TOKEN` header, or an `X-XSRF-TOKEN` header decoded from the `XSRF-TOKEN` cookie. It is validated by the `ValidateCsrfToken` middleware (called `VerifyCsrfToken` in older versions), which is in the `web` middleware group by default. GET, HEAD and OPTIONS requests are not checked.

## Who sent it?

The Laravel application. A reverse proxy or CDN has no reason to produce 419, so if you see it, the request reached PHP. The `Set-Cookie: laravel_session=...` header on the page that rendered the form (and the `XSRF-TOKEN` cookie) confirms you are dealing with Laravel's session layer.

## Fix it, in order of likelihood

1. **The form has no token.** Every non-GET form needs `@csrf` (or `<input type="hidden" name="_token" value="{{ csrf_token() }}">`). For fetch or axios calls, send `X-CSRF-TOKEN` from a `<meta name="csrf-token" content="{{ csrf_token() }}">` tag; Laravel's default axios setup already sends `X-XSRF-TOKEN` on same-origin requests.
2. **The session expired while the page was open.** The session lifetime defaults to 120 minutes of inactivity. Set it in `.env`:

   ```bash
   SESSION_LIFETIME=720
   ```

   The value is minutes and is read by `config/session.php`. For long-lived forms, refresh the token with a lightweight keep-alive request or catch the 419 in JavaScript and reload.
3. **The session cookie is not being stored.** This shows up as 419 on every login attempt. Check these values:

   ```bash
   SESSION_DOMAIN=.example.com   # must match the host; use null for host-only
   SESSION_SECURE_COOKIE=true    # only when the site is served over HTTPS
   SESSION_SAME_SITE=lax         # "none" requires Secure
   ```

   `SESSION_SECURE_COOKIE=true` on an HTTP-only site means the browser drops the cookie, so each request starts a new session. Behind a TLS-terminating proxy, configure trusted proxies so Laravel sees HTTPS; otherwise it may not set `Secure` correctly. See [Secure](https://howhttpworks.com/cookies/secure) and [SameSite](https://howhttpworks.com/cookies/same-site).
4. **The session is not shared across servers.** The `file` driver writes to `storage/framework/sessions` on one machine. With several app servers or containers behind a load balancer, use `SESSION_DRIVER=redis`, `database` or `memcached`. Also check the sessions directory is writable.
5. **A cache is serving a stale form.** A CDN or full-page cache that stores HTML containing a `csrf_token` hands every visitor the same token and no matching session. Bypass the cache for pages with forms and for responses that set cookies.
6. **`APP_KEY` changed or differs between servers.** Session cookies are encrypted with it; a mismatch makes every cookie unreadable, so every request is a new session. Run `php artisan config:clear` after changing it.
7. **A third-party POST has no way to carry a token.** Payment webhooks and similar callbacks cannot send one. In Laravel 11 and later:

   ```php
   ->withMiddleware(function (Middleware $middleware): void {
       $middleware->validateCsrfTokens(except: [
           'stripe/*',
       ]);
   })
   ```

   In older apps, add the URI to `$except` in `app/Http/Middleware/VerifyCsrfToken.php`. Do not disable CSRF protection globally.
8. **SPA on another domain.** With Sanctum, call `/sanctum/csrf-cookie` first and make sure `SANCTUM_STATEFUL_DOMAINS` lists the frontend host. A cross-site frontend also runs into SameSite cookie rules.

## Reproduce and verify

```bash
# Expect 419: no token and no session
curl -i -X POST https://app.example.com/profile -d 'name=test'

# Ask for JSON to see the message
curl -i -X POST https://app.example.com/profile -H 'Accept: application/json' -d 'name=test'
```

To test the happy path, request the form with `-c jar.txt`, extract the `_token`, and post it back with `-b jar.txt`. If that works but the browser still gets 419, the cookie handling in the browser path is the problem.

## Related codes

[403](https://howhttpworks.com/status-codes/403) is what an authorization policy returns, and [401](https://howhttpworks.com/status-codes/401) means authentication is needed. 419 says the request could not be proven to come from your own page. Validation failures in Laravel are `422`, not 419.
