Laravel Queue Worker & Scheduler Updates in v13

Laravel Queue Worker & Scheduler Updates in v13

Laravel 13 introduces refined queue worker resource controls and task scheduling updates, including percentage-based memory caps (queue:work --memory), pre-timeout job notifications, global single-server schedule definitions (Schedule::alwaysOnOneServer()), and queue-specific failed job clearing. This guide covers how to implement these capabilities to make background processes more stable and predictable across deployment environments.

Overview of Queue Worker & Scheduler Updates

Managing background queue workers and scheduled tasks in production requires balancing resource limits, task duration, and server synchronization. Recent releases in the Laravel 13 framework line—specifically Laravel v13.33.0, Laravel v13.34.0, and Laravel v13.35.0—address core execution challenges in worker runtime monitoring and schedule configuration.

Key additions include:

  • Dynamic Worker Memory Limits: Worker memory limits can now be declared as percentage values (for example, --memory=80%) rather than fixed megabyte values.
  • Pre-Timeout Job Notifications: Jobs can receive signal notifications when approaching timeout thresholds, allowing graceful cleanup before process termination.
  • Opt-In Worker Timeout Survival: Option to prevent worker process destruction when an individual job times out.
  • Targeted Failed Queue Flushing: The queue:flush command accepts a --queue= option to clear failed jobs selectively per queue.
  • Global Single-Server Scheduling: Schedule::alwaysOnOneServer() enforces single-server execution globally across scheduled jobs.
  • Enhanced Scheduler Telemetry: schedule:list --json output includes the onOneServer attribute, and long-running scheduled commands can listen for schedule:interrupt.
  • Job Event Metrics: JobProcessed events now carry job execution duration metrics.

Who Needs These Changes and What Problems They Solve

1. Multi-Tier Container and Cloud Hosting Teams

In containerized environments (such as Docker, Kubernetes, or AWS ECS), container memory limits vary by deployment environment. Previously, specifying --memory=128 meant updating deployment configurations whenever container allocations changed. Passing --memory=80% allows queue processes to adapt automatically to host or container memory limits.

2. Applications Running Long-Running Batch Jobs

When processing video rendering, PDF generation, or third-party API batch syncs, unexpected job timeouts normally force the worker to terminate abruptly via OS signals. Jobs can now capture pending timeout signals to roll back transactions, close network sockets, or mark job states before termination occurs.

3. Multi-Server Distributed Deployments

Deploying web servers behind load balancers requires scheduled tasks to run on only one instance to prevent duplicate job dispatching. Previously, developers had to append ->onOneServer() to every scheduled item in routes/console.php. The global directive enforces this rule by default across all scheduled commands.


Feature Comparison: Older Approaches vs. Laravel 13

Feature Focus Older Approach (Laravel 12 & Earlier) Laravel 13 Approach Practical Benefit
Worker Memory Limits Fixed MB integers only (e.g., --memory=128) Fixed MB integers or percentages (e.g., --memory=75%) Prevents OOM kills when scaling container RAM without modifying artisan scripts.
Job Timeout Notification Worker terminated job via SIGALRM without notifying application logic notifyJobOfSignal dispatches notification to job before exit Allows explicit state updates, database rollbacks, or alert logging.
Failed Queue Flushing queue:flush cleared all failed jobs across every queue queue:flush --queue=high-priority Prevents accidental deletion of failed jobs belonging to unrelated business queues.
Single Server Scheduling Configured per individual task: $schedule->command(...)->onOneServer() Set globally: Schedule::alwaysOnOneServer() Eliminates duplicate execution risks when developers forget per-task configuration.
Scheduler Inspection schedule:list --json only showed command strings and cron expressions Included onOneServer boolean state Enables automated CI/CD checks for schedule server lock settings.

Practical Guide: Implementing the New Background Worker Features

Step 1: Upgrading Worker Process Supervisors

If you run queue workers under Supervisor or Systemd, update your worker startup commands to leverage adaptive memory limits. When running web apps on production hosts managed by process managers alongside NGINX Explained for Beginners, ensuring background tasks do not exhaust system memory is crucial.

Update your /etc/supervisor/conf.d/laravel-worker.conf configuration file:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --memory=80%
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600

When queue:work runs with --memory=80%, Laravel evaluates ini_get('memory_limit'). If the PHP CLI memory limit is set to 512M, the worker automatically restarts upon reaching approximately 409.6M of RAM usage.

# Reload Supervisor configuration in production
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart laravel-worker:*

Step 2: Graceful Job Handling Prior to Timeout

Laravel 13 adds timeout notification support to queued job instances. When a job approaches its designated --timeout limit, a system signal (such as SIGALRM on POSIX systems) triggers a job hook if implemented.

Create a resilient export job class (app/Jobs/GeneratePdfReport.php):

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

class GeneratePdfReport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $timeout = 120; // 2 minutes timeout
    public string $tempFilePath;

    public function __construct(public int $reportId)
    {
        $this->tempFilePath = "tmp/report_{$reportId}.tmp";
    }

    public function handle(): void
    {
        Log::info("Starting PDF report generation for ID: {$this->reportId}");

        // Simulate step-by-step heavy processing
        for ($step = 1; $step <= 100; $step++) {
            $this->processChunk($step);
            usleep(1000000); // 1 second delay per chunk
        }
    }

    /**
     * Hook invoked when worker sends a timeout warning signal.
     */
    public function timeoutAt(): void
    {
        Log::warning("Job ID {$this->reportId} is approaching timeout threshold. Cleaning temporary files.");
        
        if (Storage::disk('local')->exists($this->tempFilePath)) {
            Storage::disk('local')->delete($this->tempFilePath);
        }
    }

    private function processChunk(int $step): void
    {
        Storage::disk('local')->append($this->tempFilePath, "Completed chunk {$step}");
    }
}

Step 3: Granular Failed Queue Operations

In high-volume applications, a bug in an isolated secondary queue (such as webhook deliveries) can generate thousands of failed entries. Previously, running artisan queue:flush deleted all failed records across all queues.

Laravel 13 adds a --queue= option to isolate operations:

# Inspect failed jobs before flushing
php artisan queue:failed

To delete failed entries originating exclusively from the webhooks queue while keeping payments or notifications queues intact:

php artisan queue:flush --queue=webhooks

You can verify the flush output in your terminal logs:

Flushing failed jobs for queue [webhooks]...
Flushed 42 failed jobs successfully.

Step 4: Configuring Global Single-Server Task Scheduling

When managing multi-node application clusters, define your global scheduler configuration inside routes/console.php or within a service provider (app/Providers/AppServiceProvider.php).

Edit routes/console.php:

<?php

use Illuminate\Support\Facades\Schedule;

// Force all scheduled commands in this application to run on one server only
Schedule::alwaysOnOneServer();

Schedule::command('reports:daily')
    ->dailyAt('01:00');

Schedule::command('cache:prune-stale-tags')
    ->hourly();

Schedule::command('sync:inventory')
    ->everyFiveMinutes();

To inspect schedule properties programmatically or in continuous integration (CI) environments, request JSON output:

php artisan schedule:list --json

Example JSON output:

[
  {
    "command": "php artisan reports:daily",
    "expression": "0 1 * * *",
    "timezone": "UTC",
    "user": null,
    "maint_mode": false,
    "onOneServer": true,
    "behindOnOneServer": false
  },
  {
    "command": "php artisan cache:prune-stale-tags",
    "expression": "0 * * * *",
    "timezone": "UTC",
    "user": null,
    "maint_mode": false,
    "onOneServer": true,
    "behindOnOneServer": false
  }
]

Step 5: Handling Interrupt Signals in Scheduled Commands

Long-running scheduled maintenance tasks can now handle cancellation signals during application deployments or queue pauses using schedule:interrupt detection.

Create a console command (app/Console/Commands/ReindexSearchIndex.php):

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;

class ReindexSearchIndex extends Command
{
    protected $signature = 'search:reindex';
    protected $description = 'Rebuild search index in batches with interrupt handling';

    public function handle(): int
    {
        $this->info('Starting search reindex...');
        $totalBatches = 50;

        for ($i = 1; $i <= $totalBatches; $i++) {
            // Check if schedule interrupt signal or cache flag was dispatched
            if (Cache::has('schedule:interrupt')) {
                $this->warn('Interrupt signal detected. Stopping reindex cleanly at batch ' . $i);
                return Command::SUCCESS;
            }

            $this->performReindexingBatch($i);
            usleep(500000);
        }

        $this->info('Search reindex completed successfully.');
        return Command::SUCCESS;
    }

    private function performReindexingBatch(int $batchNumber): void
    {
        // Perform batch operations
    }
}

Troubleshooting & Common Edge Cases

1. Worker Percentage Memory Calculation Fails

  • Symptom: queue:work --memory=80% exits immediately or throws an exception regarding invalid memory limit.
  • Cause: The PHP CLI environment has memory_limit = -1 set in php.ini (unlimited memory).
  • Fix: Provide a explicit CLI memory limit in php.ini or set it inline when starting the worker process:
php -d memory_limit=512M artisan queue:work --memory=80%

2. Job Timeout Notifications Not Firing (SIGALRM missing)

  • Symptom: timeoutAt() lifecycle hook is not executed before the job times out.
  • Cause: The pcntl extension is either disabled or running under a Windows host environment, where POSIX process signals are unavailable.
  • Fix: Ensure pcntl is enabled in your Linux/macOS PHP CLI environment:
php -m | grep pcntl

If missing, install or enable ext-pcntl in your Dockerfile or server environment:

RUN docker-php-ext-install pcntl

3. alwaysOnOneServer() Cache Mutex Issues

  • Symptom: Scheduled tasks do not run on any server after configuring Schedule::alwaysOnOneServer().
  • Cause: Single-server scheduling relies on a central default cache store (e.g., Redis or Memcached). If servers use localized file or array cache drivers, each server believes it owns the lock or fails to acquire a shared lock key.
  • Fix: Verify CACHE_STORE in .env resolves to a shared central driver across all cluster nodes:
CACHE_STORE=redis

Verification and Testing Steps

To confirm your queue worker runtime and scheduler features operate correctly:

  1. Validate Memory Limit Dynamic Triggers: Run a queue worker in verbose mode with low memory allocation:

    php -d memory_limit=128M artisan queue:work --memory=50% -v
    

    Dispatch jobs that consume memory and confirm worker restarts when memory usage passes ~64 MB.

  2. Verify Job Processed Telemetry: Listen for JobProcessed events in your application code or automated tests:

    use Illuminate\Queue\Events\JobProcessed;
    use Illuminate\Support\Facades\Event;
    
    Event::listen(JobProcessed::class, function (JobProcessed $event) {
        Log::info("Job {$event->job->resolveName()} processed in {$event->duration} ms");
    });
    
  3. Test Single-Server Schedule Lock: Run php artisan schedule:list --json in your build step to assert that `

Discussion

Comments

No comments yet. Be the first to comment.

Browse the project library

Projects to explore next

Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.

All projects