Everyone is going to talk about the AI SDK and the passkeys, and fair enough, they demo well. But the part of Laravel 13 I keep turning over is much less glamorous: the release ships two contradictory answers to the same question, namely where your configuration is supposed to live. You can now decorate a queued job with a #[WithQueue] attribute that declares its connection, queue name, tries and timeout right on the class. Or you can leave the class bare and declare all of that in a service provider with Queue::route(), mapping job classes to connections and queues in one central place. Same knobs, two homes, and the framework has no opinion about which one you pick. So I'll offer mine.

AppServiceProvider.php
<?php

// inside AppServiceProvider::boot()
use App\Jobs\GeneratePdfReport;
use App\Jobs\ProcessPodcast;
use Illuminate\Support\Facades\Queue;

Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'media');
Queue::route(GeneratePdfReport::class, connection: 'sqs', queue: 'exports');

First, credit where due: the upgrade story is genuinely good. If you're on PHP 8.3 or newer, the core team is promising no breaking changes to your application code and an upgrade that fits inside a coffee break. Attributes now work in more than fifteen places across the framework, so Eloquent models can carry #[Table], #[Fillable] and #[Hidden] instead of a stack of protected properties, and all of it is opt-in. Your existing models keep working untouched. That restraint deserves a nod, because it means the interesting decisions in this release are stylistic ones you get to make deliberately rather than migrations forced on you by a deadline.

Here's my rule, and I'd defend it in any code review: deployment decisions go in the provider, domain shape goes on the class. Which connection a job runs on, whether PDF exports land on SQS while media processing stays on Redis, that changes when your ops situation changes, not when your business logic does. Routing it centrally via Queue::route() means the day you move exports to a different backend, you touch one provider and zero job classes, and every dispatch site stays a plain ProcessPodcast::dispatch($podcast). A model's fillable list, by contrast, is intrinsic to what the thing is. It belongs on the class, and the attribute syntax finally makes it read like a declaration instead of clutter.

The honest counter-argument is discoverability, and I've been burned by it in other ecosystems. When a new teammate opens GeneratePdfReport and sees nothing about where it runs, they have to know the provider exists. Anyone who has spelunked through a Spring codebase looking for the one XML file that explains everything knows this pain, and the attribute version keeps the whole story greppable in a single file. I still land on central routing for infra, though, because the failure mode of scattered config is worse: fifty job classes each carrying their own connection string is fifty places to forget when you migrate queues, and I have watched exactly that produce a Friday incident where half the jobs went to the old cluster.

The sleeper feature of the release, for my money, is Cache::touch(). Extending a key's TTL used to mean reading the value and writing it back, two network round trips plus serialization, all to bump a timestamp. Now Cache::touch('user_session:123', 3600) sends Redis a single EXPIRE, hits Memcached with its native TOUCH, and runs a plain UPDATE on the database driver. If you've ever built a sliding-window rate limiter or kept a hot dashboard metric warm, you know how often this pattern comes up. It's a small API that does one thing and maps cleanly onto what each store can actually do. More of this, please.

The flashier items are real too, I just doubt they'll reshape your daily diff the way the config question will. The Laravel AI SDK goes stable alongside the release, so with PostgreSQL and pgvector you get whereVectorSimilarTo() straight in the query builder, with embedding generation wired to OpenAI, Anthropic or Ollama. Passkey login via WebAuthn now ships in Fortify and the starter kits, and since the private key never leaves the user's device, credential stuffing against your login endpoint stops being a threat worth the attacker's time. Add the Reverb database driver for scaling websockets over MySQL or PostgreSQL without a Redis box, Http::pool() defaulting to a concurrency of 2 instead of crawling serially, and teams returning to the starter kits with tab-isolated routing so switching teams in one browser tab doesn't corrupt another.

One side note on attributes, because it's easy to miss how far PHP has come here. Go developers have been cramming metadata into struct tags for a decade, raw strings that the compiler cheerfully ignores until runtime. PHP attributes are actual classes with named arguments, typed, autocompleted, refactorable. Laravel leaning into them across the framework is less a Laravel feature than the ecosystem finally cashing a check the language wrote back in PHP 8.0. It took a while, but the syntax we're getting is nicer than what several supposedly more modern languages settled for.

So that's my line: providers for anything an ops decision can change, attributes for anything that defines the domain object itself. But I know teams who will reject the split outright and standardize on one style just to keep reviews simple, and I can't call them wrong. Where do you land? Are you routing jobs centrally, decorating them with #[WithQueue], or writing a Pint rule to forbid whichever one lost the argument at your standup? Tell me below, especially if you've already tried mixing both in one codebase.