laravel-queue

Auto-invoked skill

Create jobs, events, listeners with rate limiting

Trigger Keywords

This skill automatically activates when Claude detects these keywords:

queue job event listener horizon dispatch

Overview

The laravel-queue skill provides expertise for background job processing. It covers job creation, event-driven architecture, rate limiting, retries, and monitoring with Horizon.

What This Skill Provides

  • Job Creation - Queueable jobs with proper patterns
  • Event/Listener - Event-driven architecture
  • Rate Limiting - Throttle job processing
  • Retry Logic - Backoff, max attempts, failure handling
  • Batching - Process related jobs together
  • Horizon - Queue monitoring and configuration

Example Conversations

# Creating a job
"Create a job to process large CSV imports in chunks"

# Event-driven
"Set up events for when a user places an order"

# Rate limiting
"Limit my email sending job to 100 per minute"

# Failure handling
"What happens when a job fails? How do I handle it?"

Job Example

<?php

namespace App\Jobs;

use App\Models\Order;
use App\Services\PaymentGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;

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

    public int $tries = 3;
    public int $backoff = 60; // seconds
    public int $timeout = 120;

    public function __construct(
        public Order $order
    ) {}

    public function middleware(): array
    {
        return [
            new RateLimited('payments'),
            new WithoutOverlapping($this->order->id),
        ];
    }

    public function handle(PaymentGateway $gateway): void
    {
        $result = $gateway->charge($this->order);

        if ($result->failed()) {
            $this->fail($result->error());
        }
    }

    public function failed(\Throwable $exception): void
    {
        // Notify admin, log, etc.
    }
}

Dispatching Jobs

// Dispatch immediately to queue
ProcessPayment::dispatch($order);

// Dispatch with delay
ProcessPayment::dispatch($order)->delay(now()->addMinutes(10));

// Dispatch to specific queue
ProcessPayment::dispatch($order)->onQueue('payments');

// Dispatch after response sent
ProcessPayment::dispatchAfterResponse($order);

// Conditional dispatch
ProcessPayment::dispatchIf($order->isPending(), $order);

// Chain jobs
Bus::chain([
    new ProcessPayment($order),
    new SendReceipt($order),
    new UpdateInventory($order),
])->dispatch();

Rate Limiting

// In AppServiceProvider
RateLimiter::for('payments', function (object $job) {
    return Limit::perMinute(100);
});

// Per-user rate limiting
RateLimiter::for('emails', function (object $job) {
    return Limit::perMinute(10)->by($job->user->id);
});

Unique Jobs

use Illuminate\Contracts\Queue\ShouldBeUnique;

class ProcessWebhook implements ShouldQueue, ShouldBeUnique
{
    public int $uniqueFor = 3600; // seconds

    public function uniqueId(): string
    {
        return $this->webhook->id;
    }
}

Common Pitfalls

// 1. Not Serializing Properly - Models must use SerializesModels
// Bad - serializes entire model data
public function __construct(public array $orderData) {}

// Good - only serializes model ID
use SerializesModels;
public function __construct(public Order $order) {}

// 2. Missing Failed Job Handler
public function failed(\Throwable $e): void
{
    Log::error('Job failed', [
        'order_id' => $this->order->id,
        'error' => $e->getMessage(),
    ]);
}

// 3. Not Setting Appropriate Timeouts
public int $timeout = 30; // seconds
public bool $failOnTimeout = true;

// 4. Infinite Retry Loops
public int $tries = 3;
public array $backoff = [10, 60, 300]; // 10s, 1m, 5m

// 5. Heavy Data in Queued Jobs
// Bad - stores large data
ProcessCsv::dispatch($csvContent);

// Good - store file path
ProcessCsv::dispatch($filePath);

// 6. Not Restarting Workers After Deploy
// php artisan queue:restart

Package Integration

  • laravel/horizon - Redis queue monitoring dashboard
  • spatie/laravel-failed-job-monitor - Failed job notifications
  • spatie/laravel-schedule-monitor - Schedule monitoring

Best Practices

  • Use specific queues for different priorities
  • Set appropriate timeouts and retry limits
  • Handle failures gracefully
  • Monitor queue length and worker health
  • Use Horizon for Redis queue monitoring
  • Serialize only what's needed
  • Use unique jobs to prevent duplicates
  • Test job dispatch and handling

Testing Jobs

use Illuminate\Support\Facades\Queue;

it('dispatches payment job on order', function () {
    Queue::fake();

    $order = Order::factory()->create();
    $order->process();

    Queue::assertPushed(ProcessPayment::class, function ($job) use ($order) {
        return $job->order->id === $order->id;
    });
});

// Test job execution
it('charges the order', function () {
    $order = Order::factory()->create();

    (new ProcessPayment($order))->handle(app(PaymentGateway::class));

    expect($order->fresh()->status)->toBe('paid');
});

Related Commands