Queues
⏳ Queues
Job classes directly, going further than Chapter 4's brief ShouldQueue preview.
Queue Drivers
// .env QUEUE_CONNECTION=database // or redis, sqs, sync
database / redis
Real queue backends — jobs sit in a table or Redis list until a worker process picks them up, the same broker role Redis/RabbitMQ play for Celery.
sync: No Worker Needed
Runs jobs immediately, in the same process, no queue or worker at all — genuinely convenient for local development. Celery has an "eager mode" for similar behavior, but it's a less first-class, more manual configuration.
🛠️ Creating a Job
php artisan make:job GenerateBookReport
// app/Jobs/GenerateBookReport.php namespace App\Jobs; use App\Models\Book; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class GenerateBookReport implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public Book $book // the ACTUAL model — see below ) {} public function handle(): void { logger()->info("Report: {$this->book->title}, \${$this->book->price}"); } }
A Genuinely Better Answer to the Django Course's Gotcha
The Django project's Celery chapter had a whole gotcha dedicated to this: never pass a model instance to a task — pass its ID and re-fetch inside the task, because the instance gets serialized as a stale snapshot. Laravel's SerializesModels trait solves this automatically:
Celery's Manual Discipline vs Laravel's Automatic Handling
Celery (Django Course)
# Passing the model directly is a documented anti-pattern — # the developer MUST remember to pass an ID instead: @shared_task def generate_report(book_id): # NOT book book = Book.objects.get(pk=book_id) # re-fetch by hand
Laravel: SerializesModels
public function __construct(public Book $book) {} // The trait stores only the model's class + ID when queuing, // then automatically re-fetches a FRESH instance from the database // the moment the worker actually runs the job. Nothing to remember.
This is a genuine ergonomic win: the exact discipline the Django course had to teach as a rule to memorize is handled automatically by a trait every generated Job already includes.
Dispatching a Job
use App\Jobs\GenerateBookReport; GenerateBookReport::dispatch($book); // queues it, returns immediately
🔁 Retries and Failure Handling
class GenerateBookReport implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; public int $backoff = 10; // seconds between retries public function __construct(public Book $book) {} public function handle(): void { /* ... */ } public function failed(\Throwable $exception): void { logger()->error("Report generation failed for book {$this->book->id}: {$exception->getMessage()}"); } }
Directly parallel to Celery's autoretry_for/retry_backoff/max_retries — same idea, class properties instead of decorator arguments.
Running the Queue Worker
php artisan queue:work
# Directly analogous to: celery -A mysite worker
💻 Coding Challenges
Challenge 1: Create and Dispatch a Job
Generate a ProcessOrder job that takes an Order model in its constructor and logs a summary line, then dispatch it from a controller after creating an order.
Goal: Practice the generate-job, dispatch workflow, passing the model directly.
Challenge 2: Add Retry Logic
Extend the ProcessOrder job with $tries = 3 and a failed() method that logs the failure, and explain what happens differently compared to a job with no retry configuration at all.
Goal: Practice configuring retries and handling permanent failure.
Challenge 3: Diagnose a Silently Stuck Queue
Given a controller that calls ProcessOrder::dispatch($order) but the order confirmation never seems to actually process, list two possible causes covered in this chapter and how to check for each.
Goal: Practice the same "queued but nothing consuming it" debugging instinct from the Django course's Celery chapter.
sync DriverTwo distinct traps, both worth knowing: first, exactly like Celery, dispatch() queues a job silently even with zero workers running — if jobs never seem to execute, check that php artisan queue:work is actually running before assuming the job code is broken. Second, a Laravel-specific one: local .env commonly defaults QUEUE_CONNECTION to sync for easy local development (no worker needed) — it's easy to forget to switch this to database or redis before deploying, which means every "queued" job actually runs synchronously in production, silently defeating the entire point and blocking real requests on slow work.
🎯 What's Next
Background work is now handled correctly — the next chapter speeds up repeated work instead: Caching, Laravel's cache facade compared to Django's cache framework.