Jobs
What a queued job class is, how make:job generates it, the three ways to dispatch one, and what gets serialised.
ProcessUpload::dispatch(...$constructorArguments): Illuminate\Foundation\Bus\PendingDispatchA job is a class that holds one unit of work plus the data that work needs. Instead of doing something slow inside a request, you push the job onto a queue and return a response. A separate process runs it later.
Creating one
php artisan make:job ProcessUpload writes app/Jobs/ProcessUpload.php. The generated class implements Illuminate\Contracts\Queue\ShouldQueue and uses the Illuminate\Foundation\Queue\Queueable trait. ShouldQueue is the marker that says queue this rather than run it now. Queueable supplies the static dispatch method, the queue, connection and delay setters, and model serialisation. The constructor takes the data. handle() does the work and is called by the worker.
Dispatching
These three lines are the same call:
ProcessUpload::dispatch($upload);
dispatch(new ProcessUpload($upload));
Bus::dispatch(new ProcessUpload($upload));
The first is the shorthand from the trait. It builds the object from the arguments you pass and returns an Illuminate\Foundation\Bus\PendingDispatch, which is why onQueue() and delay() chain after it.
What crosses the wire
The job object is serialised into a string and stored. Scalars and arrays are written out as they are. An Eloquent model is not: only its class name and key are stored, and the worker re-queries the row before handle() runs. That is why a job carries an id rather than a model. Anything that cannot be serialised, such as a closure property or an open file handle, fails at dispatch time.
Example
<?php
// app/Jobs/ProcessUpload.php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Upload;
use App\Services\ThumbnailMaker;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessUpload implements ShouldQueue
{
use Queueable;
/**
* Only the upload's key is serialised onto the queue.
*/
public function __construct(
public Upload $upload,
) {}
/**
* Typed parameters here are resolved from the service container,
* not from the dispatch call.
*/
public function handle(ThumbnailMaker $maker): void
{
$path = $maker->make($this->upload->path, width: 320);
$this->upload->update([
'thumbnail_path' => $path,
'status' => 'processed',
]);
}
}When to use it
- Resize an uploaded image after responding to the upload request.
- Call a slow third-party API without making the user wait for it.
- Recalculate a report for one account when its data changes.
- Move a long loop over thousands of rows out of a web request.
More examples
Dispatching from a controller, with options
onQueue picks which named queue inside the connection the job lands on. delay takes a Carbon instance or a number of seconds; now()->addMinutes(10) and the Laravel 13 form now()->plus(minutes: 10) are the same value. afterCommit holds the dispatch until the surrounding database transaction commits, which is the fix for a worker that picks the job up before the row exists. You can make that the default for a connection with 'after_commit' => true in config/queue.php.
<?php
// app/Http/Controllers/UploadController.php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Jobs\ProcessUpload;
use App\Models\Upload;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UploadController extends Controller
{
public function store(Request $request): RedirectResponse
{
$upload = Upload::create([
'path' => $request->file('file')->store('uploads'),
'status' => 'pending',
]);
// Shorthand from the Queueable trait.
ProcessUpload::dispatch($upload);
// The same thing written out, with options chained on the
// PendingDispatch that dispatch() returns.
ProcessUpload::dispatch($upload)
->onQueue('media')
->delay(now()->addMinutes(10))
->afterCommit();
return redirect()->route('uploads.index');
}
}Dispatch conditionally, or not at all
dispatchIf and dispatchUnless read better in a chain of guard clauses and are exactly the if statements below them. dispatchSync is useful when you want the job's code path but need the result before responding, for example in a console command or a test.
<?php
use App\Jobs\ProcessUpload;
// Shorthand
ProcessUpload::dispatchIf($upload->isVideo(), $upload);
ProcessUpload::dispatchUnless($account->suspended, $upload);
// The longhand each one desugars to
if ($upload->isVideo()) {
ProcessUpload::dispatch($upload);
}
if (! $account->suspended) {
ProcessUpload::dispatch($upload);
}
// Runs now, in this process, ignoring the queue entirely.
ProcessUpload::dispatchSync($upload);The data a job carries is just ids
This is the shape of a queue payload with the framework taken away: a list of keys. Seeing it this way makes the serialisation rule obvious. The worker gets 11 and 13 and reads the current rows, so whatever the rows look like when the job runs is what handle() sees.
<?php
$uploads = collect([
['id' => 11, 'status' => 'pending'],
['id' => 12, 'status' => 'processed'],
['id' => 13, 'status' => 'pending'],
]);
$ids = $uploads->where('status', 'pending')->pluck('id')->values();
// In the application this would be:
// $ids->each(fn (int $id) => ProcessUpload::dispatch(Upload::find($id)));
echo $ids->implode(',');
// Output:
// 11,13
Discussion