Laravel 13 shipped on March 17, 2026, and the official upgrade guide estimates the work at ten minutes. That number is honest for a clean Laravel 12 app. It is not honest for the applications I usually get called into: six years of accumulated packages, a custom cache store somebody wrote in 2021, and a test suite that passes because half of it is skipped.
This post is the Laravel 13 upgrade walkthrough I use with clients. I will cover what actually changed, which breaking changes generate real work versus which ones you can read past, how the path differs depending on whether you are on Laravel 10, 11, or 12, and what to check before you touch composer.json. Every version fact here comes from the official Laravel 13 upgrade guide and the Laravel support policy. Where something depends on your codebase, I say so.
Where the Support Clock Stands in September 2026
Before scoping anything, know how much runway you have. Laravel gives every major release 18 months of bug fixes and 2 years of security fixes.
| Version | Supported PHP | Released | Bug Fixes Until | Security Fixes Until |
|---|---|---|---|---|
| 10 | 8.1 – 8.3 | Feb 14, 2023 | Aug 6, 2024 | Feb 4, 2025 (EOL) |
| 11 | 8.2 – 8.4 | Mar 12, 2024 | Sep 3, 2025 | Mar 12, 2026 (EOL) |
| 12 | 8.2 – 8.5 | Feb 24, 2025 | Aug 13, 2026 | Feb 24, 2027 |
| 13 | 8.3 – 8.5 | Mar 17, 2026 | Q3 2027 | Mar 17, 2028 |
Three things follow from this table as of today:
- Laravel 10 and 11 are fully end of life. No bug fixes, no security fixes. If you are on either, the upgrade is a security line item, not a roadmap item.
- Laravel 12 stopped receiving bug fixes on August 13, 2026. You still get security patches until February 2027, so you have a planning window rather than an emergency.
- Laravel 13 requires PHP 8.3 minimum and supports through PHP 8.5.
That last point matters more than it looks. Laravel 12 supported PHP 8.2; Laravel 13 does not. If you are on 8.2, the PHP jump is a separate workstream that has to land first.
What's Actually New in Laravel 13
Laravel 13's release notes are explicit that the team prioritized minimizing breaking changes this cycle, shipping quality-of-life improvements throughout the year instead. That framing holds up when you read the upgrade guide: three high-impact items, two medium, and a long tail of low and very-low changes.
The headline additions:
- Laravel AI SDK. A first-party, provider-agnostic API for text generation, tool-calling agents, embeddings, audio, and images.
- JSON:API resources. First-party resource classes that handle resource object serialization, relationship inclusion, sparse fieldsets, links, and spec-compliant headers. If you have hand-rolled JSON:API compliance on top of API resources, this is worth a look alongside the patterns in my post on Laravel API development best practices.
- Semantic and vector search. Native vector query support, including
whereVectorSimilarTo()on the query builder against PostgreSQL withpgvector. - Queue routing by class.
Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts')centralizes routing rules that previously lived scattered across job constructors. - Expanded PHP attributes.
#[Middleware]and#[Authorize]on controllers, plus#[Tries],#[Backoff],#[Timeout], and#[FailOnTimeout]on jobs. Cache::touch(). Extend an item's TTL without reading and rewriting the value.
None of these are required to adopt. All of them are additive. Which is the point: the upgrade itself is small, and the new capabilities are opt-in work you can schedule separately.
The Laravel 13 Breaking Changes That Actually Bite
Here is the full impact classification from the official guide, so you can triage rather than read 30 sections.
| Change | Impact | Likely to affect you? |
|---|---|---|
| Updating dependencies | High | Always |
| Updating the Laravel installer | High | Only if you use the CLI installer |
| Request forgery protection rename | High | If you reference VerifyCsrfToken anywhere |
Cache serializable_classes |
Medium | If you cache PHP objects |
upsert() with MySQL/MariaDB |
Medium | If you use upsert() |
| Cache prefixes / session cookie names | Low | Only without explicit config |
Session serialization set to json |
Low | If you sync the skeleton config |
JobAttempted event payload |
Low | If you listen to it |
QueueBusy property rename |
Low | If you listen to it |
| Domain route precedence | Low | Multi-tenant / subdomain apps |
Container::call nullable defaults |
Low | Rare |
MySQL DELETE with JOIN + LIMIT |
Low | Rare but nasty |
| Polymorphic pivot table naming | Low | Custom pivot classes only |
Str factories reset between tests |
Low | Test suites with UUID fakes |
Manager::extend callback binding |
Low | Custom drivers |
| Pagination Bootstrap view names | Low | Bootstrap 3 pagination |
symfony/polyfill-php85 conflicts |
Low | Legacy global helpers |
Let me walk through the ones I actually spend time on.
1. CSRF Middleware Renamed to PreventRequestForgery
This is the change most likely to produce a red test suite. Laravel's CSRF middleware is now PreventRequestForgery and adds request-origin verification using the Sec-Fetch-Site header. VerifyCsrfToken and ValidateCsrfToken survive as deprecated aliases, but any direct class reference should be updated.
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
// Laravel <= 12.x
->withoutMiddleware([VerifyCsrfToken::class]);
// Laravel >= 13.x
->withoutMiddleware([PreventRequestForgery::class]);
Find every reference before you upgrade:
rg -n "VerifyCsrfToken|ValidateCsrfToken" app/ bootstrap/ config/ routes/ tests/
The middleware configuration API also gains preventRequestForgery(...) in bootstrap/app.php. The origin-verification behavior is the part worth testing manually: if you have webhook endpoints, embedded iframes, or a decoupled frontend posting cross-origin, exercise those paths on a staging deploy rather than trusting your unit tests.
2. Cache serializable_classes Defaults to false
The Laravel 13 skeleton's config/cache.php now sets serializable_classes to false. This hardens cache unserialization against PHP deserialization gadget chains if your APP_KEY leaks. Applications that deliberately cache PHP objects have to declare an allow-list:
// config/cache.php
'serializable_classes' => [
App\Data\CachedDashboardStats::class,
App\Support\CachedPricingSnapshot::class,
],
The failure mode here is quiet and delayed. Nothing breaks at deploy time; it breaks the first time a cached object is read back. Audit for object caching before you flip this:
rg -n "Cache::(put|forever|remember|rememberForever)\s*\(" app/
If your application caches arrays and scalars only, take the false default and move on.
3. Session serialization Changes to json
The Laravel 13 skeleton sets config/session.php's serialization option to json. If you sync your config files with the new skeleton, changing this from php to json invalidates every active session. Every logged-in user gets kicked out.
That is sometimes fine and sometimes a support-ticket avalanche. If you need a seamless deploy, keep 'serialization' => 'php' during the upgrade and flip it in a separate, announced release. If your application does not store PHP objects in the session, json is the more secure setting and worth adopting on your own schedule.
4. upsert() Now Validates uniqueBy
Laravel 13 throws an InvalidArgumentException when uniqueBy is empty, instead of generating invalid SQL. This applies even on MySQL and MariaDB, where the driver ignores uniqueBy and uses the table's primary and unique indexes anyway.
// Throws InvalidArgumentException in Laravel 13
DB::table('stats')->upsert($rows, [], ['views']);
// Correct
DB::table('stats')->upsert($rows, ['user_id', 'date'], ['views']);
Teams that developed against MySQL often have empty or placeholder uniqueBy arrays because it never mattered. Grep for it:
rg -n "upsert\(" app/ database/ --type php -A 2
5. MySQL DELETE With JOIN, ORDER BY, and LIMIT
Previously, ORDER BY and LIMIT clauses could be silently dropped from joined deletes. Laravel 13 compiles them into the generated SQL. On MySQL and MariaDB variants that do not support that syntax, you now get a QueryException where you previously got a successful, unbounded delete.
Read that twice. The old behavior was deleting more rows than you asked for. The new exception is a bug report you should be grateful for, but it will surface as a production error in batch cleanup jobs. If you run chunked deletes, this is the section to reread, and it pairs with the indexing and query-shape work I cover in Laravel database optimization.
6. Queue Event Payload Changes
Two small renames that break listeners silently if you are not on strict types:
// JobAttempted
// Laravel <= 12.x
$event->exceptionOccurred; // bool
// Laravel >= 13.x
$event->exception; // Throwable|null
// QueueBusy
// Laravel <= 12.x: $event->connection
// Laravel >= 13.x: $event->connectionName
If you push queue telemetry into Sentry, Nightwatch, or a custom dashboard, these are the lines to fix.
7. The symfony/polyfill-php85 Global Function Conflict
Laravel 13 depends on symfony/polyfill-php85. On PHP below 8.5, that polyfill defines global functions including array_first() and array_last() unless something defined them earlier during bootstrap.
This collides with laravel/helpers and with hand-rolled global helper files, and the semantics differ. The historical array_first() accepted a callback and returned the first matching element; the polyfilled version returns the first element of the array, full stop. That is a behavior change disguised as a name collision, and it will not throw.
use Illuminate\Support\Arr;
// Safe across PHP versions
Arr::first($array, fn ($value) => $value->isActive());
Audit before upgrading:
rg -n "function (array_first|array_last)" app/ bootstrap/ helpers/
composer show laravel/helpers
The Version-by-Version Upgrade Path
Laravel's upgrade guides assume you are arriving from the immediately previous release. That assumption drives everything below.
Laravel 12 → 13
The straightforward case. Update 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"
}
}
Then:
composer update --with-all-dependencies
composer global update laravel/installer # if you use the CLI installer
php artisan config:clear && php artisan cache:clear
php artisan test
For most Laravel 12 apps this genuinely is a short job. The time goes into third-party packages and the CSRF rename, not the framework.
Laravel 11 → 13
Two hops, run separately. Do not merge them into one branch.
git checkout -b upgrade/laravel-12
# apply the 11.x -> 12.x guide, run the suite, deploy to staging, merge
git checkout -b upgrade/laravel-13
# apply the 12.x -> 13.x guide
The Laravel 12 hop is small but has two items that catch people. First, HasUuids now returns UUIDv7 (ordered) values; if you need the old behavior, swap to HasVersion4Uuids. Second, Carbon 2 support was removed, so all Laravel 12+ apps run Carbon 3. There is also the local filesystem disk default root change to storage/app/private and the image validation rule no longer accepting SVGs. Full list is in the Laravel 12 upgrade guide.
Laravel 10 → 13
Three hops, and the 10 → 11 hop carries most of the risk. Laravel 11 raised the PHP floor to 8.2 and introduced the restructured application skeleton. The skeleton restructure is opt-in, which is the single most useful thing to know here: you can upgrade the framework without rewriting app/Http/Kernel.php into bootstrap/app.php on the same day. Do the version bump first, adopt the new structure later as its own refactor.
Read the Laravel 11 upgrade guide section by section rather than relying on any summary, including this one. And if you are coming from something older than 10, the staged approach in my legacy Laravel migration playbook applies before any of this does.
Should You Skip a Version?
No, and here is the specific reason rather than the general one: Laravel's upgrade guides only document the delta from the immediately previous release. There is no 10 → 13 guide. Skipping means every change from the skipped release becomes undocumented territory you reverse-engineer from changelogs and stack traces.
The compromise I use with clients is sequential upgrades with a single deploy. Run 10 → 11 → 12 → 13 as separate commits on separate branches, each with a green test suite, then ship the whole chain in one release. You get documented steps and a clean git bisect surface without three production deploys.
The exception: if you are jumping several majors and the intermediate versions are all EOL anyway, there is no value in deploying each hop. There is still value in committing each hop.
Pre-Upgrade Audit Checklist
Run all of this before you change a single constraint. It takes an afternoon and it is where the estimate actually comes from.
# 1. What PHP are you actually running in production?
php -v
# 2. Which of your packages block Laravel 13?
composer why-not laravel/framework 13.0
# 3. Abandoned or unmaintained dependencies
composer outdated --direct
composer audit
# 4. Direct references to renamed APIs
rg -n "VerifyCsrfToken|ValidateCsrfToken" app/ bootstrap/ config/ routes/ tests/
rg -n "exceptionOccurred" app/
rg -n "QueueBusy" app/
rg -n "upsert\(" app/ database/
rg -n "pagination::(simple-)?default" resources/
# 5. Custom implementations of contracts that gained methods
rg -n "implements .*(Store|Repository|Dispatcher|ResponseFactory|MustVerifyEmail)" app/
# 6. Test coverage reality check
php artisan test --coverage --min=0
Item 2 is the single most predictive command in the list. composer why-not will tell you in seconds whether this is a one-day job or a three-week job, because package compatibility, not framework code, is what stalls most upgrades.
Also confirm your static analysis baseline is current before you start, so new errors are attributable to the upgrade:
vendor/bin/phpstan analyse --generate-baseline
git add phpstan-baseline.neon && git commit -m "chore: refresh phpstan baseline pre-upgrade"
Package and Dependency Compatibility
The framework is rarely the blocker. Work through these in order:
- First-party packages. Horizon, Telescope, Sanctum, Cashier, Scout, Pulse, Nova. These generally ship Laravel 13 support at or near release, but Nova in particular has historically been the long pole for teams on older licenses.
- Octane. If you run Octane, verify your server runtime alongside the framework bump and re-run your load profile after. Long-lived workers surface state leaks that a per-request app never shows, which is the same class of issue I dig into in my Laravel Octane performance guide.
- Community packages. For anything unmaintained, decide now: fork it, replace it, or inline the 200 lines you actually use. Do not let an abandoned package hold the whole upgrade hostage.
- PHP extensions. Moving to PHP 8.4 or 8.5 can strand extensions with lagging builds. Check them in CI, not on your laptop.
For a stuck package, the fastest read is usually to check whether the constraint is genuinely incompatible or merely unversioned:
composer why vendor/package
composer require vendor/package:"dev-main as 3.0" --no-update # temporary, for testing only
Testing Strategy
Two rules. First, the suite must be green on your current version before you start, or you have no signal. Second, turn on deprecation reporting so Laravel 13's deprecated aliases show up as noise you can clear.
For PHPUnit 12:
<!-- phpunit.xml -->
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
failOnDeprecation="true"
failOnWarning="true"
failOnNotice="true"
displayDetailsOnTestsThatTriggerDeprecations="true">
<testsuites>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
</phpunit>
For Pest 4, the same flags apply through phpunit.xml, and you can run a deprecation-only pass:
vendor/bin/pest --fail-on-deprecation
Note the Str factories change while you are here: Laravel 13 resets custom Str factories during test teardown. If you set a deterministic UUID or ULID factory once in a base test case and relied on it persisting across test methods, move that into setUp() or a per-test hook.
Beyond the automated suite, manually exercise: login and logout, any cross-origin POST, file uploads, queue workers under load, scheduled commands, and anything that reads from cache written by the previous version.
Two accelerators worth knowing about. Laravel Shift is the community-maintained automated upgrade service that Laravel's own guide recommends, and it handles the mechanical diff well. Laravel Boost ^2.0 ships an /upgrade-laravel-v13 slash command as a first-party MCP server, usable from Claude Code, Cursor, OpenCode, Gemini, or VS Code, that walks an AI assistant through guided upgrade prompts. I use both, then review every hunk by hand. The workflow I follow for that review is the same one in my practitioner's guide to legacy codebases with Claude Code, and I run an AI-assisted code review pass over the resulting diff before it goes near a PR.
Deploy and Rollback Plan
Upgrades fail at deploy more often than in the editor. Plan for it.
- Tag the pre-upgrade release.
git tag pre-laravel-13 && git push --tags. Your rollback is a redeploy of that tag, not a revert commit written under pressure. - Watch the cache prefix change. If you rely on framework-level fallback prefixes rather than explicit config, Laravel 13's hyphenated defaults (
app-cache-instead ofapp_cache_) mean every key misses on first read. SetCACHE_PREFIX,REDIS_PREFIX, andSESSION_COOKIEexplicitly in.envbefore deploying. - Drain queues first. Serialized job payloads written by Laravel 12 workers get read by Laravel 13 workers. Let the queue empty, deploy, then restart workers.
- Deploy off-peak, then flip session serialization separately. Bundling the
jsonsession change with the framework bump means one deploy that both changes behavior and logs everyone out. - Have a database rollback path. If the upgrade includes migrations, confirm
migrate:rollbackactually works on a staging copy of production data. On platform deploys like Laravel Vapor the application rollback is fast, but schema changes are still one-way unless you planned them to be reversible.
Realistic Timeline and Effort
Ranges from projects I have actually run. Assumes a single senior developer and a suite that passes before you start.
| Starting point | App profile | Engineering effort | Calendar time |
|---|---|---|---|
| Laravel 12 | Small, few packages, good coverage | 4 – 8 hours | 1 – 2 days |
| Laravel 12 | Mid-size, 30+ packages, partial coverage | 2 – 4 days | 1 week |
| Laravel 11 | Mid-size, standard package set | 4 – 7 days | 1.5 – 2 weeks |
| Laravel 11 | Large, Octane or Nova, thin coverage | 2 – 3 weeks | 4 – 6 weeks |
| Laravel 10 | Mid-size, skeleton left as-is | 1.5 – 2.5 weeks | 3 – 4 weeks |
| Laravel 10 | Large, low coverage, unmaintained packages | 4 – 8 weeks | 2 – 3 months |
Calendar time runs longer than engineering effort because of code review, staging soak, and stakeholder scheduling. Budget the gap. For the full commercial picture, including the drivers that push these numbers around and the upgrade-versus-rewrite decision, see what a Laravel upgrade actually costs.
When Not to Upgrade Yet
Reasons to wait that I consider legitimate:
- You are mid-launch. A framework upgrade during a feature freeze for a major release is a self-inflicted risk. Ship, then upgrade.
- You are on PHP 8.2 with no plan. Laravel 13 needs 8.3 minimum. Sequence the PHP work first as its own release.
- A blocking package has no Laravel 13 branch and no fork budget. Wait, or scope the replacement as separate work.
- You have effectively no test coverage. Writing characterization tests around your critical paths first is not a delay, it is the cheaper half of the project.
Reasons to wait that are not legitimate: "it's working fine," and "we'll do it next quarter" said for the third consecutive quarter. Laravel 10 and 11 have no security support. Laravel 12 has until February 2027. The cost of this work rises every month you defer it, because the gap you have to cross keeps widening.
Key Takeaways
- Laravel 13 released March 17, 2026, requires PHP 8.3 minimum, and receives security fixes until March 17, 2028.
- The upgrade itself is small by design. Three high-impact changes: dependencies, the installer, and the
VerifyCsrfToken→PreventRequestForgeryrename. - The changes that cause real incidents are the quiet ones: cache
serializable_classes, the sessionserializationflip that logs everyone out, cache prefix defaults, and thesymfony/polyfill-php85global function collision. - Do not skip versions. Commit each hop separately; you can still ship them as one deploy.
composer why-not laravel/framework 13.0is the fastest honest estimate you will get. Run it first.- Laravel 10 and 11 are end of life. Laravel 12 loses security support in February 2027.
Always work from the official Laravel 13 upgrade guide as the source of truth for your specific application. Anything a blog post tells you, including this one, is a map and not the territory.
Need a Hand With Yours?
I have spent 14 years in PHP and Laravel, and a good share of it running upgrades for teams who would rather not lose two weeks to one. If you want a second opinion on scope, a pre-upgrade audit, or someone to run the whole thing while your team keeps shipping features, get in touch through the contact form. Tell me your current Laravel version, your PHP version, and paste the output of composer why-not laravel/framework 13.0. That is usually enough for a useful first answer.

Richard Joseph Porter
Senior Laravel Developer with 14+ years of experience building scalable web applications. Specializing in PHP, Laravel, Vue.js, and AWS cloud infrastructure. Based in Cebu, Philippines, I help businesses modernize legacy systems and build high-performance APIs.
Need Help Upgrading Your Laravel App?
I specialize in modernizing legacy Laravel applications with zero downtime. Get a free codebase audit and upgrade roadmap.
Related Articles
PHP 8.3 to 8.5 for Laravel Teams: The Upgrade Before the Upgrade
Upgrade PHP for Laravel the right way: a staged 8.3 to 8.4 to 8.5 path, the PHP 8.4 breaking changes that bite, CI matrix, canary rollout, and rollback.
What a Laravel Upgrade Actually Costs: Scope, Timeline, and When to Rewrite Instead
A practical Laravel upgrade cost model for CTOs and founders: real hour and dollar ranges, timeline expectations, audit commands, and an upgrade-vs-rewrite scorecard.
Migrate Legacy Laravel Apps: A Staged Upgrade Playbook
Upgrade Laravel 4.x-8.x safely: legacy factories and Laravel 11 compatibility, testing patterns, and staged migration lessons from 14 years of PHP.