Laravel v13.40.0 was released on 3 November 2026, and it is one of the more practical point releases this year. It adds alerts for slow queue jobs and scheduled tasks, a way to check whether a read replica has caught up, cleaner ways to place middleware, strict casting for request input, and a security fix for the Concurrency process driver.

This tutorial walks through each feature with code you can drop into a Laravel 13 app. At the end there’s a short upgrade checklist.

Upgrade first

If your app uses Concurrency::run() with the default process driver, upgrade now. More on that below.

composer update laravel/framework
php artisan --version   # should show 13.40.0 or later

1. Get alerted when queue jobs run too long

Laravel already had DB::whenQueryingForLongerThan() for slow queries. Now queued jobs get the same treatment with Queue::whenJobRunsLongerThan(). Register it in a service provider’s boot() method:

use Carbon\CarbonInterval;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;

public function boot(): void
{
    Queue::whenJobRunsLongerThan(CarbonInterval::seconds(90), function ($job, $duration) {
        Log::warning("{$job->resolveName()} took {$duration}ms");
    });
}

Things to know:

  • The callback listens to the JobAttempted event, so it also fires for attempts that fail or are released, not just successful ones.
  • If you pass a plain number instead of a CarbonInterval, it is read as milliseconds.
  • The $duration passed to your callback is also in milliseconds.

In a real app you’d probably send this to Slack or your error tracker instead of the log, and include the queue name so you can see which worker pool is struggling.

2. Get alerted when scheduled tasks run too long

The scheduler gets a matching method, Schedule::whenTaskRunsLongerThan(). Put it in routes/console.php next to your schedule definitions:

use Carbon\CarbonInterval;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:generate')->hourly();

Schedule::whenTaskRunsLongerThan(CarbonInterval::minutes(45), function (Event $task, float $runtime) {
    Log::warning("Scheduled task [{$task->getSummaryForDisplay()}] took {$runtime}s");
});

Watch the units: here a plain number means seconds, not milliseconds. The callback runs after a task finishes or fails.

Two related scheduler and worker changes ship in the same release:

  • The scheduler now dispatches a ScheduledTaskSkipped event when an onOneServer() task already ran on another server.
  • queue:work now shows RELEASED instead of DONE for jobs released back to the queue, for example by WithoutOverlapping or RateLimited middleware.

3. Check whether a read replica has your write

If you use read/write database connections, you’ve probably hit this bug: a user saves something, gets redirected, and the next page (read from a replica) doesn’t show the change yet. Laravel 13.38 added the PreventStaleDatabaseReads middleware, which sends reads to the primary for a fixed time after a write. Laravel 13.40 adds a more precise tool: commit IDs.

use Illuminate\Support\Facades\DB;

// After a write, on the primary
$order = $user->orders()->create($attributes);
$commitId = DB::getLastCommitId();

// Store it, e.g. in the session
session(['last_commit' => $commitId]);

// Later, on a request that reads from a replica
if (DB::hasCommitId(session('last_commit'), wait: 2)) {
    // replica has the write, safe to read
}

hasCommitId() can check right away, or wait up to the given number of seconds. A mergeCommitIds() method combines several IDs, so you can wait for all of them at once.

Database support is limited, so check yours:

  • MySQL with GTIDs enabled
  • MariaDB with the binary log enabled
  • PostgreSQL. Waiting on PostgreSQL requires PostgreSQL 19.

Other drivers throw a RuntimeException, so wrap the call if your code might run against SQLite in tests.

4. Insert middleware before or after a built-in one

Before this release, if you needed your middleware to run right after one of Laravel’s defaults, you had to redefine the whole stack in bootstrap/app.php. Now there are helpers:

->withMiddleware(function (Middleware $middleware) {
    $middleware->insertAfter(TrustProxies::class, StripCrossOriginReferer::class);
    $middleware->insertBefore(TrimStrings::class, SanitizeStrings::class);
})

For a group such as web, use insertBeforeInGroup() and insertAfterInGroup(). Also, replace() and replaceInGroup() now accept an array, so you can swap several middleware in one call.

This matters most for middleware that depends on order. A sanitiser that must run before TrimStrings, or anything that needs the real client IP after TrustProxies, now takes one line instead of a hand-maintained list.

5. Strict request input casting

Laravel’s typed input helpers are convenient but forgiving. Without the new flag, ?count=abc123 cast with integer() gives 0, and an array like ?name[]=chris passed to string() throws an “Array to string conversion” error. The new strict argument returns your default for invalid values instead:

// GET /items?count=abc123
$request->integer('count', 5);               // 0
$request->integer('count', 5, strict: true); // 5

It works on string(), str(), boolean(), integer() and float(), on requests and anything else that uses the InteractsWithData trait. In strict mode, boolean() only accepts 1, true, on, yes, 0, false, off and no.

A good pattern for pagination and filters:

$perPage = $request->integer('per_page', 15, strict: true);
$perPage = max(1, min($perPage, 100)); // still clamp to a safe range

$onlyActive = $request->boolean('active', false, strict: true);

Strict casting isn’t a replacement for validation on forms you save to the database. It is a good fit for query-string options where a sane default is better than an error.

6. The Concurrency security fix

This release fixes a remote code execution issue in Concurrency::run() with the process driver. The parent process read each task’s result from the child process output and unserialized it. A task that printed user-controlled text could forge a result. The GitHub advisory GHSA-63hm-g96r-2m2q rates it Low, because it needs a specific insecure pattern in app code. The fix is still worth having.

The child process now signs its result with a random per-task key, and the parent ignores anything that doesn’t verify. The advisory says the fix was also back-ported to 12.x. Even after upgrading, avoid echoing raw user input inside concurrent tasks.

Other fixes worth knowing

  • When the database connection drops inside a transaction, Laravel now discards the dead PDO and reconnects. Before, long-running queue workers or Octane processes could get stuck with “There is already an active transaction”.
  • A lock wait timeout or deadlock in a nested transaction now rolls back to that transaction’s savepoint.

Upgrade checklist

  1. Run composer update laravel/framework and confirm 13.40.0 or later.
  2. Add Queue::whenJobRunsLongerThan() and Schedule::whenTaskRunsLongerThan() alerts, and watch the units.
  3. Search your code for Concurrency::run( and review what those tasks print.
  4. Replace hand-written middleware stacks with insertBefore() / insertAfter() where it simplifies things.
  5. Use strict: true on query-string casts that should fall back to a default.

Takeaway

Laravel 13.40 has no headline feature, but it improves day-to-day operations: you hear about slow jobs before users complain, replica lag becomes something you can check, and messy query strings stop turning into zeros. Upgrade, add the two timing alerts, and you’ll get value from it right away.

Sources: Laravel News: Slow Job Alerts and Replica Commit IDs in Laravel 13.40 · laravel/framework CHANGELOG · Security advisory GHSA-63hm-g96r-2m2q