Laravel 13.17: Route Metadata, Postgres Pooler Support, and More
Laravel 13.17 landed this week with a handful of quality-of-life improvements that are worth knowing about. None of them are headline-grabbing framework redesigns, but they're exactly the kind of additions that quietly make day-to-day development smoother.
First-Class Route Metadata
The headline feature is route metadata support. You can now attach arbitrary key-value data to any route definition using the new metadata() method:
Route::get('/dashboard', DashboardController::class)
->middleware('auth')
->metadata('feature', 'dashboard')
->metadata('team', 'core');
You can read that metadata back from the matched route at runtime:
$route = request()->route();
$feature = $route->metadata('feature'); // 'dashboard'
This is genuinely useful for things like feature flags, audit logging, and authorization policies that need to know which route was matched without parsing URI patterns. Previously you'd have had to stash this in route names or custom middleware — now it's first-class.
Postgres Transaction Pooler Integration
If you're running your Postgres database behind a transaction-mode connection pooler (PgBouncer is the most common), you've probably run into issues with prepared statements. Transaction poolers don't maintain state between queries, which breaks PDO's prepared statement cache.
Laravel 13.17 adds a dedicated pgsql driver option to disable prepared statements automatically when using a pooler:
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'transaction_pooler' => true, // new
],
Setting transaction_pooler to true tells Laravel to disable prepared statements for that connection. This removes a common pain point for anyone running Laravel on Laravel Cloud's managed Postgres or a self-hosted PgBouncer setup.
The dev:list Command
Laravel 13.16 introduced php artisan dev as a single command to spin up your local development processes. 13.17 follows it up with php artisan dev:list, which prints a table of every process dev is currently configured to run:
$ php artisan dev:list
Process Command
─────────────────────────────
server php artisan serve
vite npm run dev
queue php artisan queue:work
Small thing, but useful when you've configured a handful of processes and can't remember whether you added the queue worker.
ShouldNotRetry Exception Handler
Queue jobs that throw exceptions get retried by default, up to the job's $tries limit. Sometimes you have an exception class that represents a permanent failure — a 404 from an API, a record that no longer exists — where retrying will never succeed.
Previously you'd catch the exception and call $this->fail() manually. 13.17 adds a cleaner approach: implement the ShouldNotRetry interface on the exception itself:
use Illuminate\Contracts\Queue\ShouldNotRetry;
class ResourceNotFoundException extends \RuntimeException implements ShouldNotRetry
{
//
}
Any job that throws a ShouldNotRetry exception will be immediately marked as failed without using up its retry attempts. This is a good pattern for any exception that represents a condition that won't change on its own.
Upgrade
All of these changes are backwards-compatible and available via a standard composer update:
composer update laravel/framework
No config changes required unless you're adopting the Postgres pooler option.