5 min read

Scaling Laravel Queue Workers: Idle Shutdown and SQS Overflow Storage

How Laravel 13.10 makes queue workers cheaper to run with --stop-when-empty-for, the WorkerIdle event, and SQS overflow storage for oversized job payloads.

Featured image for "Scaling Laravel Queue Workers: Idle Shutdown and SQS Overflow Storage"

If you run Laravel on autoscaling infrastructure, your queue workers have probably cost you money sitting around doing nothing. A worker spun up for a traffic spike keeps polling an empty queue long after the work is gone, and you pay for every idle second. Laravel 13.10.0, released on June 2, 2026, ships a small cluster of queue features that target exactly this problem, plus a fix for the other queue headache that bites at scale: job payloads too big for the transport. Here is what landed and how to put it to work.

Workers That Know When to Quit

The headline addition is a new --stop-when-empty-for option on queue:work. It tells a worker to exit after it has gone a configured number of seconds without processing a single job.

php artisan queue:work --stop-when-empty-for=60

That command runs normally, but if 60 seconds pass with nothing to do, the worker shuts down on its own. This is not the same as the older --stop-when-empty flag, which exits the moment the queue is empty even once. The difference matters. --stop-when-empty is great for batch jobs where you push a pile of work and want the worker gone as soon as it drains. --stop-when-empty-for is built for steady-state systems where the queue empties and refills constantly, and you only want to scale down after a genuine lull.

The pattern this unlocks is clean autoscaling. On a platform like AWS ECS, Fargate, or Kubernetes, you let your orchestrator launch extra workers when the queue backs up, and you start each one with --stop-when-empty-for. When demand drops, workers exit themselves once the queue stays quiet for your chosen window, and your platform scales the task count down to match. No supervisor tricks, no separate teardown logic, just a worker that respects its own idleness.

Listening for Idle Workers

If you want to observe idleness rather than act on it, 13.10 also adds a WorkerIdle event. It fires when a worker checks for a job and finds the queue empty. That is a meaningful distinction from the existing JobPopping event, which fires on every single pop attempt whether or not a job comes back. WorkerIdle only fires when there was genuinely nothing to do, so it is a reliable signal of unused capacity.

use Illuminate\Queue\Events\WorkerIdle;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

Event::listen(function (WorkerIdle $event) {
    Log::info('Worker idle', [
        'connection' => $event->connectionName,
    ]);
});

Logging idle ticks lets you build a picture of how much of your worker fleet is actually busy over a day. If you find a connection that is idle 80 percent of the time, that is a strong hint you are over-provisioned and a candidate for the --stop-when-empty-for treatment above. Pair the two and you have both the data and the lever.

Related to this, 13.10 now passes the WorkerOptions object (which carries the --name flag and the rest of a worker’s configuration) into the Pausing, Resuming, Interrupted, and Looping events. Previously those events gave you no way to tell which worker instance fired them. Now a listener can branch on the worker name, which is handy when you run several differently configured pools side by side.

When Job Payloads Outgrow the Queue

The second class of problem at scale is payload size. Amazon SQS caps a message at 256 KB, and any queue with a hard size limit will eventually reject a job that carries too much data. The usual workaround is to pass an identifier instead of the data and rehydrate inside the job, but that is boilerplate you have to remember on every job that might grow.

Laravel 13.9.0 introduced an SQS overflow store to handle this automatically, and 13.10.0 rounded it out. When overflow is enabled, Laravel detects a payload that would exceed the SQS size limit, writes the body to a configured disk such as S3, and sends a small pointer through SQS in its place. The worker transparently fetches the real payload on the other side, so your job code never changes. There is also an option to store every payload regardless of size if you prefer a uniform path.

You configure it on the connection in config/queue.php. The example published with the 13.10 release looks like this:

'sqs' => [
    // ...
    'overflow' => [
        'enabled' => true,
        'disk' => 's3',
        'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false),
    ],
],

The new flush_on_clear option is the piece 13.10 added. With it on, running queue:clear will also flush the overflow store after purging SQS, so leftover S3 objects do not linger and quietly accrue storage cost. It defaults to false to preserve existing behavior. One caution from the release notes worth repeating: for most cache stores, flush() wipes the entire store, so point the overflow store at a dedicated cache rather than a shared one if you enable this. Check the queue documentation for the complete and current option list before you ship it.

A Natural Companion: The Storage Cache Driver

The same release added a storage cache driver backed by Laravel’s filesystem abstraction, which slots neatly into the overflow story. It lets you use an S3 disk as a key/value cache with no Redis or Memcached in the mix. The default config/cache.php now includes it:

'storage' => [
    'driver' => 'storage',
    'disk' => env('CACHE_STORAGE_DISK'),
    'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],

Each value is stored as a file holding a serialized payload and an expiration timestamp. It is not a replacement for an in-memory cache on the hot path, but for a dedicated overflow store, or any cache where durability matters more than microsecond latency, it is a tidy zero-dependency option.

Worth Wiring Up

None of these features are flashy, and that is rather the point. Queue infrastructure should be boring and cheap. The combination of self-terminating idle workers, an event that tells you which workers are loafing, and automatic offloading of oversized payloads removes three of the recurring papercuts that show up once your queue volume gets serious. If you are on Laravel 13, update to at least 13.10.0 and spend an afternoon matching your worker start commands to your actual traffic shape. Your infrastructure bill will thank you.

Sources