Laravel v13.10.0 introduces a storage cache driver backed by Laravel's filesystem abstraction, making it possible to use an S3 disk (or any configured disk) as a key/value cache store without additional packages. It also adds a --stop-when-empty-for queue worker option, a WorkerIdle event, schedule group lifecycle callbacks, Schema::hasForeignKey(), and several queue testing improvements.
- New
storagecache driver backed by Laravel's filesystem --stop-when-empty-forqueue worker optionWorkerIdleevent for idle worker detection- Lifecycle and output callbacks on
Schedule::group() Schema::hasForeignKey()helperqueue:failed --jsonoutput optionassertPushedOnce()testing helper- Enum queue names in
QueueFake WorkerOptionspassed to additional worker events
What's New
Storage Cache Store
A new storage cache driver uses Laravel's filesystem / Storage services to store cached values. This is primarily useful for using an existing S3 disk as a key/value cache — no Redis or Memcached required.
The default config/cache.php now includes a storage store entry:
'storage' => [ 'driver' => 'storage', 'disk' => env('CACHE_STORAGE_DISK'), 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),],
Point CACHE_STORAGE_DISK at any configured disk (including s3) and the cache driver will read and write values through Laravel's filesystem layer. Each cached value is stored as a file containing a serialized payload with an expiration timestamp.
PR: #60131 by @taylorotwell
Queue Worker Idle Stop Option
queue:work now accepts a --stop-when-empty-for option that stops the worker after it has gone a configured number of seconds without processing any jobs:
php artisan queue:work --stop-when-empty-for=60
This stops the worker if no jobs have been processed for 60 seconds. It's useful for short-lived workers, scaled-down environments, or any situation where you want workers to exit automatically when queues go quiet rather than running indefinitely.
PR: #60176 by @taylorotwell
Worker Idle Event
A new WorkerIdle event is dispatched when a queue worker checks for a job and finds the queue empty. This is distinct from JobPopping, which fires on every pop attempt regardless of whether a job was found. Listening to WorkerIdle lets you detect workers that are genuinely unused — useful for rebalancing worker capacity or logging idle time.
PR: #60134 by @jackbayliss
Worker Configuration Passed to Additional Worker Events
WorkerOptions (which includes the --name flag and other worker configuration) is now passed to the Pausing, Resuming, Interrupted, and Looping worker events. Previously these events did not include the worker's configuration, making it harder to know which worker instance was involved in a listener.
PRs: #60135, #60153 by @jackbayliss
Schedule Group Lifecycle Callbacks
Schedule::group() now supports the same lifecycle and output callback methods available on individual events. This lets you attach callbacks once for an entire group instead of repeating them on each task:
Schedule::group(function (Schedule $schedule) { $schedule->command('reports:generate'); $schedule->command('reports:email');})->onFailure(function () { // fires for any failing task in the group})->onSuccess(function () { // fires when each task in the group succeeds});
PR: #60133 by @cosmastech
Scheduled Event Instance in Callbacks
Scheduled event callbacks (such as onSuccess, onFailure, and then) can now optionally receive the Event instance as a parameter. This gives the callback direct access to the event's configuration — its command, output path, and other properties:
$schedule->command('reports:generate') ->onFailure(function (Event $event) { Log::error("Scheduled task failed: {$event->command}"); });
PR: #60144 by @cosmastech
Schema Foreign Key Existence Helper
A new Schema::hasForeignKey() method checks whether a specific foreign key constraint exists on a table, complementing the existing getForeignKeys() and hasIndex() helpers:
if (! Schema::hasForeignKey('orders', ['user_id'])) { Schema::table('orders', function (Blueprint $table) { $table->foreign('user_id')->references('id')->on('users'); });}
This is useful in migrations, package install scripts, and schema assertions where you want to avoid adding a foreign key that already exists.
PR: #60169 by @Tresor-Kasenda
JSON Output for Failed Jobs
The queue:failed Artisan command now accepts a --json flag, outputting failed jobs as JSON. Each entry includes id, connection, queue, class, and failed_at. An empty result returns []. This matches the --json support already available on route:list, db:show, queue:monitor, and other commands.
PR: #60168 by @Tresor-Kasenda
SQS Overflow Flush on Queue Clear
The SQS extended store (added in 13.9.0) now supports a flush_on_clear option. When enabled, running queue:clear will also call flush() on the configured overflow cache store after purging SQS, reclaiming storage immediately rather than waiting for TTL expiration. This matters for S3-backed stores where leftover objects incur ongoing cost:
'sqs' => [ // ... 'extended_store_options' => [ 'enabled' => true, 'disk' => 's3', 'flush_on_clear' => true, ],],
This option defaults to false to preserve existing behavior. Note that for most cache stores, flush() wipes the entire store — point overflow.store at a dedicated cache store (the new storage driver is a natural fit here) to avoid unintended data loss.
Assert Job Pushed Once
Queue::assertPushedOnce() is a more readable alternative to Queue::assertPushedTimes(JobClass::class, 1):
// beforeQueue::assertPushedTimes(ProcessOrderJob::class, 1); // afterQueue::assertPushedOnce(ProcessOrderJob::class);
PR: #60150 by @weshooper
Enum Queue Names in Fake Queue Assertions
QueueFake now normalizes enum queue names the same way the real queue driver does. Passing a UnitEnum case as a queue name to push(), size(), or pendingJobs() now works correctly, and assertions against enum queue names behave consistently with their string equivalents.
PR: #60161 by @Tresor-Kasenda
Cloud Request ID in Logs
For applications running on Laravel Cloud, the request ID is now output in log entries using a custom JSON formatter. It appears as a standalone field rather than being nested inside the Monolog context or extra blocks.
PR: #60156 by @jradtilbrook
Miscellaneous Fixes and Improvements
- Fix
starts_with/ends_withrejecting numeric values — these validation rules now correctly handle numeric input by casting to string before comparison, restoring behavior from Laravel 12 (#60120 by @aydinfatih) - URL encode paths for signed URLs — storage paths are now URL-encoded before being placed into signed route path segments, fixing temporary signed URLs that contain
?,&, or#characters (#60137 by @taylorotwell) - Delimit aggregate alias — SQL aggregate function aliases are now delimited to prevent conflicts with reserved words (#60140 by @willrowe)
- Optimize Worker queue pause check — the queue worker's pause check is now more efficient (#60109 by @jackbayliss)
- Validate against line breaks in emails — email validation now rejects values containing line breaks (#60151 by @taylorotwell)
References