Upgrade Laravel 12 to 13: What Actually Breaks
Table of contents
If you still run Laravel 12, the clock is ticking a little louder than you might think. According to the official support policy, Laravel 12 stopped receiving bug fixes on August 13, 2026, and its security fixes end on February 24, 2027. Meanwhile Laravel 13, released on March 17, 2026, keeps shipping weekly minor releases (13.35 and 13.37 landed in October). I have now moved a few apps from 12 to 13, and the good news is that this is one of the smallest major upgrades Laravel has ever had. Here is how I do it, and the handful of changes that actually bite.
Why upgrade Laravel 12 to 13 now
Laravel gives every major version 18 months of bug fixes and 2 years of security fixes. Here is the table from the release notes:
- Laravel 11: PHP 8.2 to 8.4, security fixes ended March 12, 2026.
- Laravel 12: PHP 8.2 to 8.5, bug fixes ended August 13, 2026, security fixes until February 24, 2027.
- Laravel 13: PHP 8.3 to 8.5, bug fixes until Q3 2027, security fixes until March 17, 2028.
So a Laravel 12 app today gets security patches but no more bug fixes. If you hit a framework bug, it will be fixed in 13, not in 12. Four months before the security deadline is a comfortable window: you can upgrade calmly instead of in a rush in February, right before Laravel 14 is expected (major releases arrive roughly every Q1).
The official upgrade guide estimates the upgrade at 10 minutes. That is optimistic for a real project with packages, but in my experience the framework part really is small. The time goes into dependencies and tests.
Step 1: PHP 8.3 and your dependencies
Laravel 13 requires PHP 8.3 or newer. If you are still on 8.2, upgrade PHP first, deploy, and only then touch Laravel. Mixing both upgrades makes it hard to know which one broke something. If you plan to go further, my notes on PHP 8.5 apply here too.
Then update composer.json. The guide lists these constraints:
"require": {
"php": "^8.3",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0"
},
"require-dev": {
"laravel/boost": "^2.0",
"phpunit/phpunit": "^12.0",
"pestphp/pest": "^4.0"
}
Run composer update -W and read the errors. Most of the time the blocker is a third-party package that has not declared support for Laravel 13 yet. Check its repository: there is often a tagged release or an open PR. Don't fork a package just to bump a constraint unless you have no other choice.
If you use the Laravel installer, update it too (composer global update laravel/installer, or update Herd).
Step 2: the changes that actually matter
The guide lists one high-impact code change and two medium ones. These are the ones I check in every project.
CSRF middleware renamed. VerifyCsrfToken is now PreventRequestForgery, and it also verifies the request origin with the Sec-Fetch-Site header. The old names remain as deprecated aliases, so nothing breaks right away, but update direct references, especially in tests:
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
// Laravel 12
$this->withoutMiddleware([VerifyCsrfToken::class]);
// Laravel 13
$this->withoutMiddleware([PreventRequestForgery::class]);
Cache serializable_classes. The default cache config now sets serializable_classes to false, to block PHP deserialization gadget chains if your APP_KEY ever leaks. If you cache Eloquent models or other objects, list the allowed classes explicitly, or better, cache arrays:
'serializable_classes' => [
App\Data\CachedDashboardStats::class,
],
upsert() on MySQL and MariaDB. An empty uniqueBy argument now throws an InvalidArgumentException instead of generating invalid SQL. Grep for ->upsert( and make sure every call passes it.
Step 3: the low-impact traps
"Low impact" does not mean "no impact". These are the ones worth a quick search in your codebase:
- Cache prefixes and session cookie names. The framework fallback values now use hyphens (
myapp-cache-instead ofmyapp_cache_). If your config files don't define them, the upgrade silently changes your cache keys and logs every user out. SetCACHE_PREFIX,REDIS_PREFIXandSESSION_COOKIEin.envto keep the old values. - Session serialization. The new skeleton uses
'serialization' => 'json'. If you sync your config with the skeleton, switching fromphptojsoninvalidates all active sessions. It is the safer choice, but plan it. - Queue events.
JobAttemptednow exposes$exceptioninstead of the boolean$exceptionOccurred, andQueueBusy::$connectionis now$connectionName. array_first()andarray_last(). Laravel 13 pulls insymfony/polyfill-php85, which defines these global functions on PHP below 8.5. If you still use the oldlaravel/helperspackage, its callback-basedarray_first()can clash. UseArr::first().- MySQL deletes with joins.
ORDER BYandLIMITon a joined delete are no longer silently dropped, so MySQL may now throw aQueryExceptionwhere it used to run an unbounded delete. That one is a bug fix in disguise: check it.
Finally, compare the skeleton with the GitHub comparison tool to pick up config changes the guide does not list.
Automating it: Shift or Laravel Boost
Two options can do most of the mechanical work. Laravel Shift opens a pull request with the changes. And Laravel Boost, the first-party MCP server, ships an /upgrade-laravel-v13 command for AI assistants such as Claude Code or Cursor (it needs Boost ^2.0). I use the AI route on small apps and still review every line, exactly as I described in my AI coding workflow. On a large app, a reviewed PR from Shift plus a good test suite is hard to beat.
Whatever the tool, my order stays the same: PHP first, then composer update, then the high and medium changes, then the full test suite, then a staging deploy where I log in, run a queue job and clear the cache once.
What you get in return
The release notes list the new features. The ones I actually use:
- PHP attributes for controller middleware and authorization (
#[Middleware('auth')],#[Authorize(...)]) and for jobs (#[Tries],#[Backoff],#[Timeout]). - Queue routing in one place:
Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts'); Cache::touch()to extend a TTL without reading and rewriting the value.- The Laravel AI SDK, first-party JSON:API resources and vector search with PostgreSQL + pgvector (
whereVectorSimilarTo()), if your project needs them.
Conclusion
Upgrading Laravel 12 to 13 is mostly a dependency exercise: PHP 8.3, a few Composer constraints, the CSRF middleware rename, the cache serialization setting and a quick check of your cache and session prefixes. With Laravel 12 already out of bug-fix support and its security support ending in February 2027, there is no good reason to wait for the deadline. Do it this autumn, on a quiet week, with your tests green before you start.