In one sentence

A queue lets your app say “I'll do this later” by saving a job to a list, while a separate worker process picks jobs off that list and runs them in the background.

🍔At a burger counter, the cashier takes your order, gives you a ticket and moves on to the next customer. The kitchen (the worker) cooks the orders in turn. Customers aren't kept standing at the till while each burger is made.

Why it matters

Users shouldn't wait for slow work

⏱️

Snappy responses

Sending an email can take 1–3 seconds. Queue it, and the page responds in milliseconds.

🔁

Automatic retries

If the mail server hiccups, the job is retried later instead of showing the user an error.

🏗️

Heavy lifting, safely

Resizing images, generating PDFs and importing CSVs can run for minutes without hitting web request timeouts.

The big picture

From request to background job

🙂Requestuser publishes a post
📤dispatch()job saved to the queue
⚡Response“Published!” sent right away
👷Workerqueue:work runs the job
📧Donefollowers notified
The web request and the worker are separate processes. The queue (a database table or Redis list) sits between them.
Job
A small class describing one piece of work, with a handle() method.
Queue
The waiting list of jobs, stored by a driver (database, Redis, SQS…).
Worker
A long-running command (php artisan queue:work) that takes jobs from the queue and runs them.
Dispatch
Putting a job onto the queue.
Step by step

Your first queued job

  1. Configure the database queue driver

    New Laravel apps already use the database driver, and the jobs and failed_jobs tables are created by the default migrations. Check your .env:

    
              QUEUE_CONNECTION=database
            

    If the tables are missing in an older app, run php artisan make:queue-table then php artisan migrate. The sync driver runs jobs immediately (no queue), which is handy in tests. In production at scale, redis is the usual choice.

  2. Create a job with make:job

    
              php artisan make:job NotifyFollowers
            

    This creates app/Jobs/NotifyFollowers.php. The ShouldQueue interface is what tells Laravel to queue it rather than run it immediately.

    
              <?php
    
              namespace App\Jobs;
    
              use App\Models\Post;
              use App\Notifications\NewPostPublished;
              use Illuminate\Contracts\Queue\ShouldQueue;
              use Illuminate\Foundation\Queue\Queueable;
    
              class NotifyFollowers implements ShouldQueue
              {
                  use Queueable;
    
                  public function __construct(public Post $post) {}
    
                  public function handle(): void
                  {
                      foreach ($this->post->author->followers as $follower) {
                          $follower->notify(new NewPostPublished($this->post));
                      }
                  }
              }
            

    Passing a model is safe: Laravel stores only its ID in the queue and re-fetches the fresh model when the job runs.

  3. Dispatch it

    
              public function publish(Post $post)
              {
                  $post->update(['published_at' => now()]);
    
                  NotifyFollowers::dispatch($post);
    
                  return back()->with('status', 'Published!');
              }
            

    A few useful variations:

    
              NotifyFollowers::dispatch($post)->delay(now()->addMinutes(5));
              NotifyFollowers::dispatch($post)->onQueue('emails');
              NotifyFollowers::dispatchAfterResponse($post);   // tiny tasks, no worker needed
            

    Mailables and notifications can be queued too: add implements ShouldQueue to the class, or use Mail::to($user)->queue(...).

  4. Run a worker with queue:work

    Nothing happens to queued jobs until a worker is running. In development, open a second terminal:

    
              php artisan queue:work
              php artisan queue:work --queue=emails,default   # priority order
            
    ⚠️ Workers keep old code in memory

    queue:work boots your app once and keeps running, so code changes aren't picked up. Restart it after edits (or use queue:listen locally). On deploy, run php artisan queue:restart, and keep workers alive with a process manager like Supervisor.

  5. Handle retries and failed jobs

    If handle() throws an exception, the job goes back on the queue and is tried again. Control how many times and how long to wait between attempts:

    
              public int $tries = 3;
    
              public function backoff(): array
              {
                  return [10, 60, 300];   // wait 10s, then 60s, then 5 min
              }
    
              public function failed(?Throwable $e): void
              {
                  // runs after the final attempt fails: alert someone, log it
              }
            

    After the last attempt, the job is stored in the failed_jobs table. Inspect and retry from the command line:

    
              php artisan queue:failed           # list failed jobs
              php artisan queue:retry all        # push them back onto the queue
              php artisan queue:retry 5          # retry one by ID
              php artisan queue:flush            # delete all failed jobs
            
ℹ️ Growing up: Laravel Horizon

When you move to Redis, Laravel Horizon (composer require laravel/horizon) gives you a dashboard of throughput, runtimes and failures, plus code-based worker configuration. It replaces running queue:work by hand.

Common mistakes

Avoid this, do that

❌ Avoid

          foreach ($followers as $f) {
              Mail::to($f)->send(new NewPost($post));
          }
          return back();
        

200 followers × 1 second each: the user stares at a spinner for minutes, or the request times out.

✅ Do

          NotifyFollowers::dispatch($post);
          return back();
        

The response is instant, and the worker sends emails in the background with retries.

❌ Avoid

          public function __construct(
              public UploadedFile $file
          ) {}
        

Jobs are serialised into storage. Uploaded files, closures and open connections don't survive the trip.

✅ Do

          $path = $request->file('csv')->store('imports');
          ImportPosts::dispatch($path);
        

Store the file first and pass simple data (a path, an ID, a model).

💡 Pro tip: make jobs safe to run twice

Retries mean a job might run more than once. Design handle() so a second run doesn't double-charge or double-send, for example by checking “already notified?” before acting.

🏋️ Try it yourself
  1. Create a LogPostPublished job that writes to the log with Log::info(), then dispatch it from a route.
  2. Visit the route without a worker running and look at the jobs table.
  3. Start php artisan queue:work and watch the job get processed.
  4. Make handle() throw an exception, set $tries = 2, and find it with php artisan queue:failed.
  5. Fix the bug and run php artisan queue:retry all.
Recap

Key takeaways

Check yourself

Mini quiz