jobs

Queueable units of work for background processing. Jobs handle queue configuration and failure handling — they delegate business logic to Actions or Services.

SKILL.md

GitHub

Jobs

When to Use

  • A task should run outside the request/response cycle (emails, imports, webhooks, heavy calculations).
  • The operation can tolerate retries and short execution delay.
  • You need queue controls such as $tries, $backoff, $timeout, or queue selection.

When Not to Use

  • The operation must complete before returning the HTTP response.
  • The operation is trivial and queue overhead is unnecessary.
  • You need strict synchronous ordering in-process; use an Action/Service call instead.

Preconditions

  • Queue driver is configured (QUEUE_CONNECTION in .env).
  • A queue worker is available in target environments (php artisan queue:work).
  • The job delegates business logic to an existing Action or Service class.
  • Payload data is serialization-safe (IDs/scalars/DTOs), not heavy service instances or closures.

Principles

  • Jobs own delivery concerns (queue, retries, timeout, failure hooks).
  • Actions/Services own business rules and domain decisions.
  • One job class should represent one clearly named unit of background work.

Rules

  • Place jobs in app/Jobs/.
  • Use verb-noun names: ProcessInvoicePayment, SendWeeklyReport, ImportProductCsv.
  • Implement ShouldQueue for asynchronous jobs.
  • Use Dispatchable, InteractsWithQueue, Queueable, SerializesModels.
  • Declare queue behavior explicitly on the class ($queue, $tries, $backoff, $timeout).
  • Use job middleware when needed (WithoutOverlapping, RateLimited, tenant/context middleware).
  • Keep handle() thin and delegate to an Action or Service.
  • Define failed(Throwable $exception) for permanent failure handling.
  • Define dead-letter/failure strategy for critical jobs (for example failed_jobs monitoring, alerts, replay flow).
  • Use fixed backoff for predictable transient failures; use exponential backoff for overloaded/unstable dependencies.
  • Use chaining/batching intentionally for ordered pipelines or grouped bulk workflows.

Examples

namespace App\Jobs;

use App\Actions\ProcessInvoicePayment;
use App\Models\Invoice;
use Illuminate\Support\Facades\Log;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;

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

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 120;
    public string $queue = 'invoices';

    public function __construct(
        private readonly Invoice $invoice,
    ) {}

    public function handle(ProcessInvoicePayment $action): void
    {
        $action->execute($this->invoice);
    }

    public function failed(Throwable $exception): void
    {
        Log::error('Invoice payment job failed permanently.', [
            'invoice_id' => $this->invoice->id,
            'message' => $exception->getMessage(),
        ]);
    }
}
// Dispatch from a controller
class InvoicePaymentController extends Controller
{
    public function __invoke(Invoice $invoice): JsonResponse
    {
        ProcessInvoicePaymentJob::dispatch($invoice);

        return new JsonResponse(['status' => 'queued'], 202);
    }
}
// Dispatch from a listener
class QueueInvoicePayment
{
    public function handle(InvoiceApproved $event): void
    {
        ProcessInvoicePaymentJob::dispatch($event->invoice)
            ->delay(now()->addMinutes(5));
    }
}
// Synchronous dispatch for tests or strict inline flow
ProcessInvoicePaymentJob::dispatchSync($invoice);
// Backoff strategy examples
public int|array $backoff = 60; // fixed: retry every 60s
// public array $backoff = [10, 30, 90, 300]; // exponential-ish progression
// Job middleware example
public function middleware(): array
{
    return [
        new WithoutOverlapping("invoice:{$this->invoice->id}"),
    ];
}
// Chaining and batching examples
Bus::chain([
    new PrepareInvoiceDataJob($invoiceId),
    new ProcessInvoicePaymentJob($invoiceId),
])->dispatch();

Bus::batch([
    new ImportLineItemJob($rowA),
    new ImportLineItemJob($rowB),
])->dispatch();

Checklist

  • Job name clearly describes the background task.
  • handle() delegates business logic to an Action/Service.
  • Queue controls ($tries, $backoff, $timeout, $queue) are explicit.
  • failed() captures actionable context (IDs, error message, alert/log).
  • Dispatch location is intentional (controller, action, listener, scheduler).

Anti-Patterns

  • Putting business logic directly in a job's handle() method instead of delegating to an Action or Service
  • Omitting $tries, $backoff, or $timeout and relying on global queue defaults
  • Not implementing a failed() method for jobs that process critical data
  • Using jobs for work that must complete synchronously before returning a response
  • Using jobs for simple, fast operations that don't justify queue overhead
  • Passing non-serializable/heavy payloads that break queue transport or inflate memory usage

References

  • Laravel Queues
  • Related: Actions/SKILL.md — for the business logic jobs delegate to
  • Related: Services/SKILL.md — for complex orchestration delegated from jobs