Laravel 13 shipped on March 17, 2026, and the headline is that almost nothing breaks. I do not fully buy the headline. I maintain this site (a production Laravel app with the legacy pre-11 directory structure) plus client applications at Ramlit, and when I ran the upgrade checklist against those codebases, the interesting findings were not the features. They were four or five quiet changes that produce no error message at all, just wrong behavior. This guide covers both halves: what you get, and what to audit before you trust a green deploy.

The hard gate: PHP 8.3 minimum
Laravel 13 drops PHP 8.2 entirely. Minimum is 8.3, supported through 8.5. On shared hosting this is usually a dropdown in the control panel; on a server you manage, it is the first thing to schedule, because nothing else in the upgrade matters until the runtime is there. Check your composer.json platform config and any CI matrix at the same time. If you are still on Laravel 10 or 11, note that you cannot skip majors: the path is 11 to 12 to 13, one upgrade guide at a time. Coming from 12, the dependency bumps are laravel/framework to ^13.0, laravel/tinker to ^3.0, and PHPUnit to ^12.0.
The features that changed how my code reads
PHP attributes across the framework
Laravel 13 brings first-class attribute support to models, jobs, commands, and more. Model configuration that used to be scattered across properties can now sit on the class declaration:
#[Table('blog_posts')]
#[Fillable(['title', 'content', 'published_at'])]
class Post extends Model
{
// ...
}
It is optional and fully backward compatible, which is the right call. My take after trying it on a fresh service: use attributes for new code, do not run a big-bang conversion of existing models. The churn produces enormous diffs for zero behavior change, and mixed style within one model is worse than old style.
Cache::touch()
Small API, real savings. Extending a cached item's TTL used to mean fetching the value and re-storing it. Cache::touch('key', 3600) extends the TTL in place, works across drivers, and returns false if the key is missing. On this site I cache rendered post content per locale and bust it with model observers, a pattern I documented in my shared hosting optimization case study. touch() fits exactly the "content unchanged, keep it warm" path that previously forced a wasteful round trip.
The Laravel AI SDK goes first-party
The AI SDK shipped as a stable first-party package the same day as the framework: one API for text generation, tool-calling agents, embeddings, and vector stores. I had already explored the 0.3.0 pre-release in a real app, and the promotion to first-party changes the calculus: it is now a reasonable default for AI features in Laravel instead of a bet on an unproven package. Alongside it, 13 adds native vector search support and JSON:API resources, plus Queue::route() for centralizing job-to-queue mapping.
The quiet breaks I found auditing a real codebase
This is the section I wish someone had written for me. None of these throw during composer update.
CSRF middleware is renamed, and it does more now
VerifyCsrfToken is now PreventRequestForgery, and beyond the rename it adds origin verification using the browser's Sec-Fetch-Site header alongside token validation. The old name survives as a deprecated alias, so your app boots fine. Where it bites is tests: any test doing withoutMiddleware(VerifyCsrfToken::class) is now excluding an alias while the real middleware still runs.
This one is personal. Because this site kept the Laravel 10 directory structure, there is a literal app/Http/Middleware/VerifyCsrfToken.php in my repo with a $except array. Legacy-structure apps like mine have to move those exclusions to the new class, not just bump the framework version. If you have webhook endpoints excluded from CSRF, verify them first after upgrading.
Cache and session prefixes switched from underscores to hyphens
The default cache and session prefix format changed. If you never pinned CACHE_PREFIX and SESSION_PREFIX in .env, then the moment Laravel 13 hits production, every cache key misses and every active session dies. Users get logged out mid-request, and your database or Redis takes a cold-cache stampede at deploy time.
On a low-traffic blog that is a shrug. On a client app with thousands of live sessions it is an incident. The fix is one line per prefix in .env, set to the old values, before you deploy. This is the single highest-leverage line in the whole upgrade for anyone running real traffic.
Model boot() restriction
Instantiating a model while that same model is still booting now throws a LogicException. This mostly targets package authors, but I have seen the pattern in application code too: a boot() method that creates a default related record, or seeds a settings row, by newing up the model being booted. It used to work by accident. Now it fails loudly, which honestly is the better behavior, but you want to find those spots before production does. Grep your models' boot() and booted() methods for new static, ::create, and ::firstOrCreate on the same class.
DELETE with JOIN now respects ORDER BY and LIMIT
Previously, ORDER BY and LIMIT on a joined delete were silently ignored. Laravel 13 compiles and sends them to MySQL, which rejects that combination, so a query that "worked" for years can start throwing QueryException. If you have chunked cleanup jobs that join and limit, test them against 13 explicitly.
The smaller ones worth a grep
JobAttemptedevent: the booleanexceptionOccurredproperty becameexception(aThrowableor null). Listeners checking the old boolean fail silently, since null is falsy.QueueBusyevent:$connectionrenamed to$connectionName.- Routes with explicit domains now match before routes without, which can reroute requests in multi-tenant subdomain apps.
- Polymorphic pivot table name inference now pluralizes. Declare
$tableexplicitly if you relied on singular names. - Cache serialization is hardened:
serializable_classesdefaults to false, so apps storing PHP objects in cache must allowlist classes or move to arrays. - MySQL upserts with an empty
uniqueBynow throw instead of generating invalid SQL.
The upgrade order that worked for me
- Get PHP 8.3 live under your current Laravel version first, and soak it for a few days.
- Pin
CACHE_PREFIXandSESSION_PREFIXin.envbefore touching composer. - Bump the dependencies, run the suite, and fix the deprecation notices rather than silencing them.
- Grep for the quiet breaks above:
withoutMiddleware,exceptionOccurred,boot()self-instantiation, joined deletes. - Only then start adopting the new toys, attributes and
Cache::touch()first since they are zero-risk.
If you are earlier in the version ladder, my Laravel 12 installation and setup guide and the 12.51 feature roundup cover the previous rung.
Where this leaves Laravel
The release tells you where the framework is going: fewer breaking changes per major, features shipped continuously across the year, and AI promoted to a first-party concern. For those of us running Laravel in production, that is the trade we want. The cost is that "zero breaking changes" marketing makes teams skip the audit, and the breaks that remain are exactly the silent kind this post lists. Support runs with bug fixes into Q3 2027 and security fixes into Q1 2028, so there is no need to rush, but there is also no reason to be the app that mass-logs-out its users over an unpinned session prefix.
Upgrading a production app and want a second pair of eyes?
I have run this checklist against my own production site and client codebases, including the awkward legacy-structure cases. Tell me which version you are coming from and roughly how the app is structured, and I will point at the places most likely to break silently — get in touch, whether the upgrade needs an audit before it ships or a full hand-off. The quiet breaks are cheap to find before deploy and expensive after.