Laravel 13.40: Slow Job Alerts and a Security Fix

· Laravel, PHP

Table of contents

Laravel 13.40, released at the start of November 2026, is one of those minor versions worth reading line by line. It adds a simple way to get alerted when a queued job or a scheduled task runs too long, follows up on the read-replica work from 13.38 with commit IDs, and, most importantly, fixes a remote code execution issue in Concurrency::run() with the process driver. Here is what changed, the code to use it, and what I'm doing on the Laravel apps I maintain.

Update first: the Concurrency fix

The release notes describe a fix for a remote code execution issue in Concurrency::run() when it uses the process driver. Results returned by the child processes are now signed with a random key per task, and results that cannot be verified are ignored. The issue was reported by @Arr0way.

If any of your apps call Concurrency::run(), check which driver they use (process is the default in many setups) and update to 13.40.0 or later:

composer update laravel/framework
php artisan --version   # Laravel Framework 13.40.x

A quick grep -rn "Concurrency::" app/ tells you whether you're affected. Even if you aren't, a framework security fix is a good reason not to sit several minor versions behind.

Slow job alerts with Queue::whenJobRunsLongerThan()

Long-running jobs are the silent killers of queue workers: they hit timeouts, hold locks, and delay everything behind them. 13.40 adds a hook that runs a callback when a job attempt takes longer than a threshold:

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

// In a service provider's boot() method
Queue::whenJobRunsLongerThan(CarbonInterval::seconds(90), function ($job, $duration) {
    Log::warning('Slow job', [
        'job' => $job->resolveName(),
        'ms' => $duration,
    ]);
});

A few details from the release notes:

  • The threshold is a CarbonInterval or a plain number, read as milliseconds, and $duration is in milliseconds too.
  • It listens to the JobAttempted event, so it also fires for attempts that failed or were released, which is exactly when you want to know.

On a Magento-sized integration where a nightly import job pushes thousands of products, this is the kind of signal I used to build by hand with timestamps in the job itself.

The same for scheduled tasks

The scheduler gets the equivalent:

use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Schedule;

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

Note the unit difference: for the scheduler, a plain number threshold is read as seconds. The callback runs after the task finishes or fails. Also new in the scheduler: a ScheduledTaskSkipped event is dispatched when an onOneServer() task has already run on another server, which makes "why didn't my cron run?" questions easier to answer.

Read replica commit IDs

Laravel 13.38 shipped a middleware to prevent stale reads from replicas. 13.40 adds a lower-level tool: you can capture the commit ID of a write on the primary and check whether a replica has caught up with it.

$commitId = DB::getLastCommitId();

DB::hasCommitId($commitId);          // check now
DB::hasCommitId($commitId, wait: 2); // wait up to two seconds

A mergeCommitIds() method combines several commit IDs so you can wait for all of them at once. Support depends on the database: MySQL needs GTIDs enabled, MariaDB needs the binary log, PostgreSQL is supported but waiting requires PostgreSQL 19, and other drivers throw a RuntimeException. Check your replication setup before relying on it.

Other changes worth knowing

  • Middleware placement: insertBefore() and insertAfter() in bootstrap/app.php let you place a middleware next to a built-in one, such as after TrustProxies, without redefining the whole stack. insertBeforeInGroup() and insertAfterInGroup() do the same within a group like web.
  • Strict input casting: $request->integer('count', 5, strict: true) returns the default instead of 0 when the input is abc123. It works for string(), boolean(), integer() and float().
  • Transaction robustness: after a lost connection inside a transaction, Laravel now reconnects instead of reusing a dead PDO, and deadlocks in nested transactions roll back to the savepoint.
  • queue:work output now shows RELEASED for released jobs instead of DONE.

Conclusion

Laravel 13.40 is an easy update with one item that isn't optional: if you use Concurrency::run() with the process driver, update now. After that, the slow job and slow task hooks are ten lines in a service provider and pay for themselves the first time a job quietly starts taking ten minutes instead of ten seconds. I'm adding both to every app I maintain, logging to the same channel as our other alerts.