Upgrading From Laravel 12 to Laravel 13: The Practical Guide

7 min read

Laravel 13 shipped March 17, 2026. The team called it a 10-minute upgrade — and for most apps, they're right. But this laravel 13 upgrade guide exists because three config areas will silently change your app's behaviour if you miss them, and the new features are genuinely worth understanding before they land in production. Let's go.

What You Need Before the Laravel 13 Upgrade

Laravel 13 requires PHP 8.3 or higher (up to 8.5). If you're still on PHP 8.2, sort that first.

php -v

Laravel 13 also bumps several core dependencies. Update your composer.json to these minimum versions:

Package Old New
laravel/framework ^12.0 ^13.0
laravel/tinker ^2.x ^3.0
phpunit/phpunit ^11.0 ^12.0
pestphp/pest ^3.0 ^4.0

If your team uses Pest, Pest 4 drops alongside this release. Check the Pest 4 migration notes for any deprecation warnings you've been sitting on.

The Three Things to Check in Any Laravel 13 Upgrade Guide

These three changes are the most likely to affect a production app. Fix them before you deploy.

1. PreventRequestForgery Replaces VerifyCsrfToken

The middleware has been renamed and extended. VerifyCsrfToken is now PreventRequestForgery, and it adds origin-aware request verification via the Sec-Fetch-Site header on top of the existing CSRF token check.

If you reference VerifyCsrfToken anywhere — route definitions, test exclusions, middleware groups — update every occurrence:

// Before
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;

->withoutMiddleware([VerifyCsrfToken::class]);

// After
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;

->withoutMiddleware([PreventRequestForgery::class]);

Search your codebase first:

grep -r "VerifyCsrfToken" .

The new origin-check uses Sec-Fetch-Site — modern browsers send this automatically. For apps behind unusual proxies or with cross-origin requests, test this in staging before pushing to production.

2. Cache Serialisation Is Disabled by Default

Laravel 13 sets serializable_classes to false in the cache config. This prevents PHP deserialization attacks by refusing to unserialize arbitrary PHP objects from the cache.

If your app stores PHP objects in cache — Eloquent models, custom value objects, anything you've serialized with serialize() — you'll need to explicitly whitelist those classes:

// config/cache.php
'serializable_classes' => [
    App\Data\CachedDashboardStats::class,
    App\Support\CachedPricingSnapshot::class,
],

If you only cache arrays, scalars, or JSON-decoded data, leave this as false and move on.

3. Cache Prefix and Session Cookie Naming

Cache prefixes now use hyphens instead of underscores (-cache- vs _cache_). Session cookie names now use Str::snake() instead of Str::slug().

For most apps this is invisible. But if you share a Redis instance across multiple apps, or check for specific cookie names in browser tests, set these explicitly in .env:

CACHE_PREFIX=myapp-cache
SESSION_COOKIE=myapp_session

If existing sessions need to survive the deploy without logging users out, pin SESSION_COOKIE to your current value before you push.

Step-by-Step Laravel 13 Upgrade Guide

With the three checks done, the mechanical upgrade is straightforward.

1. Update composer.json:

{
    "require": {
        "laravel/framework": "^13.0",
        "laravel/tinker": "^3.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^12.0",
        "pestphp/pest": "^4.0"
    }
}

2. Run the update:

composer update

3. Publish updated config files and review the diff:

php artisan vendor:publish --tag=laravel-config --force

Check what changed with git diff config/ before committing. You want to understand every line that moved.

4. Run your test suite:

php artisan test

Failing tests are your early-warning system. Fix them before touching staging.

If your team uses Laravel Boost, the /upgrade-laravel-v13 slash command in Claude Code, Cursor, or VS Code will run an AI-assisted review of your codebase. Shift is the community option for automated migration.

New Features Worth Knowing

None of these break anything. Some are worth adopting immediately.

The Laravel AI SDK

Laravel 13 ships a first-party AI SDK. Install it:

composer require laravel/ai

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"

php artisan migrate

Add your provider key to .env — OpenAI, Anthropic, Gemini, xAI, Mistral, Groq, Ollama, and others are supported:

OPENAI_API_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...

The SDK is agent-based. Generate an agent class:

php artisan make:agent SalesCoach

Then call it:

use App\Ai\Agents\SalesCoach;

$response = SalesCoach::make()->prompt('Analyse this sales transcript...');

return (string) $response;

Structured output, tool calling, conversation memory (persisted to a database table), and embeddings are all first-class features. If you've been using Laravel Prism for AI features in your app, the concepts map across cleanly — the main difference is the native SDK has conversation persistence and agent scaffolding baked in from day one.

Queue::route() — Centralised Job Routing

Before Laravel 13, job routing was scattered across $this->onQueue() in job classes, ->onQueue() at dispatch sites, and $queue property definitions. Renaming a queue in a large app meant hunting through dozens of files.

Queue::route() fixes this. Register all job routing in a service provider:

use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessPodcast;
use App\Jobs\SendWelcomeEmail;
use App\Jobs\GenerateReport;

public function boot(): void
{
    Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
    Queue::route(SendWelcomeEmail::class, queue: 'emails');
    Queue::route(GenerateReport::class, connection: 'redis', queue: 'reports');
}

Dispatch the job normally — the routing is applied automatically:

// No ->onQueue() needed — routing is resolved from the service provider
ProcessPodcast::dispatch($podcast);

I reach for this pattern in any app with more than three or four job classes. If you're not already keeping your workers tightly configured, stop Laravel queue workers from leaking memory with --max-jobs and --max-time is worth reading alongside this.

PHP Attributes on Jobs, Controllers, and Models

Laravel 13 adds optional #[Attribute] syntax across 15+ framework locations. The most immediately useful are job attributes:

// Before — property declarations at the top of every job class
class ProcessPodcast implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 30, 60];
    public int $timeout = 120;
    public bool $failOnTimeout = true;

    // ...
}

// After — configuration visible at the class declaration
#[Tries(3)]
#[Backoff([10, 30, 60])]
#[Timeout(120)]
#[FailOnTimeout]
class ProcessPodcast implements ShouldQueue
{
    // job logic only
}

Controller middleware and method-level authorization also get attribute support:

#[Middleware('auth')]
class CommentController
{
    #[Middleware('subscribed')]
    #[Authorize('create', [Comment::class, 'post'])]
    public function store(Post $post): Response
    {
        // ...
    }
}

This is entirely opt-in — existing property declarations keep working. If your team is already using PHP 8.4 property hooks in Laravel models, this follows the same philosophy of moving configuration closer to where it's declared.

Native Passkey Authentication

Laravel 13 ships passkey (WebAuthn/FIDO2) support natively inside Fortify and the new Breeze/Jetstream starter kits. Fresh apps scaffolded with these get passkey registration and login out of the box — no third-party package required.

For existing apps using Fortify, the integration is available but requires a migration and some frontend work. I'll cover the full setup for existing apps in a dedicated post.

JSON:API Resources

First-party JSON:API-compliant resource classes are now in the framework. If you're building APIs that need to conform to the JSON:API specification, Laravel 13 handles resource serialization, relationships, sparse fieldsets, and compliant response headers without extra packages.

Vector Search

// Semantic similarity query — works with pgvector, MySQL vector, etc.
$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', 'Best wineries in Napa Valley')
    ->limit(10)
    ->get();

Native vector similarity queries are now built into the query builder for AI-powered search use cases.

Package Compatibility

The major ecosystem packages were all compatible at launch:

  • Livewire 4 — compatible
  • Inertia.js — compatible
  • Filament 3 — compatible
  • Spatie packages — compatible (verify individual package versions on Packagist)

If you're still running Livewire 3, keep the upgrades separate. Doing the Laravel upgrade and a Livewire major version bump at the same time makes it harder to isolate the source of any failures.

Gotchas and Edge Cases

Database upserts with empty uniqueBy: Laravel 13 now throws an InvalidArgumentException if you call upsert() with an empty uniqueBy array on MySQL or MariaDB. If you have upsert calls where you're passing an empty array intentionally, fix those before deploying.

Queue event property renames: If you're listening on JobAttempted or QueueBusy events, two properties were renamed:

// Old → New
$event->exceptionOccurred  // JobAttempted → $event->exception
$event->connection         // QueueBusy    → $event->connectionName

Pagination views: Bootstrap 3 pagination views were renamed. pagination::default is now pagination::bootstrap-3. If you're referencing this string explicitly in any views, update it.

Password reset subject line: Changed from "Reset Password Notification" to "Reset your password". Minor, but it will break any email tests that assert on the exact subject.

Str UUID/ULID factories: These now reset between tests. If you relied on predictable factory output across test cases, expect failures.

Wrapping Up

Most Laravel 12 apps upgrade in a single composer update plus 20-30 minutes of config review. The three breaking areas — PreventRequestForgery, serializable_classes, and cache prefix — cover the vast majority of real-world issues.

The features with the most day-to-day impact: Queue::route() for larger apps, the AI SDK if you're building any AI functionality, and PHP attributes if your team values clean job classes. Passkeys and JSON:API resources are niche but production-ready for the apps that need them.

Once you're on Laravel 13 and stable, the deployment side is just as important as the upgrade itself — zero-downtime deployments with GitHub Actions and Forge covers getting new dependency versions live without downtime. And if the new Queue::route() API has you thinking about your queue architecture, scaling Laravel queues in production covers the full Horizon and Redis production setup.

laravel
laravel-13
upgrade
queues
ai
Steven Richardson

Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.