Introduction

Creating a job class is only half the story — something has to push it onto a queue. Laravel gives you a fluent dispatch API (Job::dispatch(...)) with modifiers for delay, queue, connection, and conditional execution, plus Bus::chain() and Bus::batch() for composing multi-step workflows.

Key Concepts

  • dispatch(): Pushes a job onto the default queue.
  • dispatchSync(): Runs the job inline, bypassing the queue (useful in tests).
  • Chain: A linear sequence of jobs where each one only runs if its predecessor succeeded.
  • Batch: A set of jobs that run in parallel with a shared completion callback.

Real World Context

A podcast upload pipeline has four steps: process audio → optimize → publish → notify subscribers. If you dispatch four independent jobs, they race — the notification might fire before the publish finishes. Chaining them guarantees ordering. A CSV import with 10,000 rows is the opposite: each row is independent and should run in parallel — that's a batch.

Deep Dive

Basic Dispatching

php
use App\Jobs\ProcessPodcast;

// In a controller
public function store(Request $request)
{
    $podcast = Podcast::create($request->validated());

    // Dispatch to queue
    ProcessPodcast::dispatch($podcast);

    return redirect()->route('podcasts.index')
        ->with('status', 'Podcast is being processed!');
}

Dispatch Methods

php
// Standard dispatch (queued)
ProcessPodcast::dispatch($podcast);

// Dispatch if condition is true
ProcessPodcast::dispatchIf($shouldProcess, $podcast);

// Dispatch unless condition is true
ProcessPodcast::dispatchUnless($skipProcessing, $podcast);

// Dispatch synchronously (bypasses queue)
ProcessPodcast::dispatchSync($podcast);

// Dispatch after response is sent (equivalent to ->onConnection('deferred'))
ProcessPodcast::dispatchAfterResponse($podcast);

Delayed Dispatching

php
// Delay by minutes
ProcessPodcast::dispatch($podcast)
    ->delay(now()->addMinutes(10));

// Delay by specific time
SendReminder::dispatch($user)
    ->delay(now()->addHours(24));

// Delay until specific datetime
PublishPost::dispatch($post)
    ->delay($post->scheduled_at);

Specifying Queue and Connection

php
// Dispatch to specific queue
ProcessPodcast::dispatch($podcast)
    ->onQueue('processing');

// Dispatch to specific connection
ProcessPodcast::dispatch($podcast)
    ->onConnection('redis');

// Both
ProcessPodcast::dispatch($podcast)
    ->onConnection('redis')
    ->onQueue('high');

Chain Jobs

Run jobs in sequence:

php
use Illuminate\Support\Facades\Bus;

Bus::chain([
    new ProcessPodcast($podcast),
    new OptimizeAudio($podcast),
    new PublishPodcast($podcast),
    new NotifySubscribers($podcast),
])->dispatch();

// With error handling
Bus::chain([
    new ProcessPodcast($podcast),
    new PublishPodcast($podcast),
])->catch(function (Throwable $e) {
    // Handle chain failure
    Log::error('Podcast chain failed', ['error' => $e->getMessage()]);
})->dispatch();

Job Batching

Process multiple jobs as a batch:

php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch([
    new ImportCsv($file1),
    new ImportCsv($file2),
    new ImportCsv($file3),
])->then(function (Batch $batch) {
    // All jobs completed successfully
    Log::info('Import completed!');
})->catch(function (Batch $batch, Throwable $e) {
    // First failure detected
    Log::error('Import failed', ['error' => $e->getMessage()]);
})->finally(function (Batch $batch) {
    // Batch finished (success or failure)
})->name('CSV Import')
  ->allowFailures()
  ->dispatch();

// Get batch ID for tracking
$batchId = $batch->id;

Checking Batch Status

php
use Illuminate\Support\Facades\Bus;

$batch = Bus::findBatch($batchId);

$batch->id;               // Batch ID
$batch->name;             // 'CSV Import'
$batch->totalJobs;        // Total jobs in batch
$batch->pendingJobs;      // Jobs waiting
$batch->processedJobs();  // Jobs completed
$batch->failedJobs;       // Jobs failed
$batch->progress();       // Percentage complete
$batch->finished();       // Is batch done?
$batch->cancelled();      // Was batch cancelled?

Adding Jobs to Batch

php
// Inside a batched job
public function handle(): void
{
    if ($this->batch()->cancelled()) {
        return;
    }

    // Add more jobs to the batch
    $this->batch()->add([
        new ProcessChunk($this->chunk),
    ]);
}

Dispatching from Controllers

php
class PodcastController extends Controller
{
    public function store(StorePodcastRequest $request)
    {
        $podcast = Podcast::create($request->validated());

        ProcessPodcast::dispatch($podcast);

        return response()->json([
            'message' => 'Podcast uploaded and queued for processing',
            'podcast' => $podcast,
        ], 201);
    }

    public function publish(Podcast $podcast)
    {
        Bus::chain([
            new OptimizePodcast($podcast),
            new GenerateThumbnail($podcast),
            new PublishPodcast($podcast),
        ])->dispatch();

        return back()->with('status', 'Publishing in progress...');
    }
}

Common Pitfalls

  1. Using dispatch() inside a database transaction — the job may run before the transaction commits, so the worker won't see the row you just created. Use dispatch(...)->afterCommit() or the $afterCommit = true property on the job.
  2. Dispatching a chain when you wanted a batch — a chain is sequential: each job waits for the previous one. A batch runs in parallel. Using a chain for independent work wastes time; using a batch for dependent work breaks ordering.

Best Practices

  1. Use Bus::chain() for linear workflows, Bus::batch() for parallel work — the right primitive makes the intent obvious and gives you the right failure semantics for free.
  2. Prefer ->onConnection('deferred') over dispatchAfterResponse() for new code — both send the job to run after the HTTP response, but the connection form integrates with Queue::route() and the rest of the connection machinery.

Summary

  • Job::dispatch($data) is the standard push; modifiers add delay, queue, and connection.
  • dispatchIf / dispatchUnless are one-liners for conditional dispatch.
  • Bus::chain([...])->dispatch() runs jobs sequentially; Bus::batch([...])->dispatch() runs them in parallel.
  • dispatchAfterResponse() and ->onConnection('deferred') both run after the HTTP response — prefer the connection form in new code.
✓ Completed