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
phpuse 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:
phpuse 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:
phpuse 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
phpuse 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
phpclass 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
- 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. Usedispatch(...)->afterCommit()or the$afterCommit = trueproperty on the job. - 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
- 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. - Prefer
->onConnection('deferred')overdispatchAfterResponse()for new code — both send the job to run after the HTTP response, but the connection form integrates withQueue::route()and the rest of the connection machinery.
Summary
Job::dispatch($data)is the standard push; modifiers add delay, queue, and connection.dispatchIf/dispatchUnlessare 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.