This one fails immediately, in the request, before the job is ever queued:
Exception: Serialization of 'Closure' is not allowed
Others in the same family show up as Serialization of 'PDO' is not allowed, or a complaint about a CurlHandle. The job never reached the queue, and the stack trace points at your dispatch call rather than anything inside handle().
The whole job object goes into the payload
When you dispatch, Laravel serializes the job instance itself and stores that string as the message. Whatever you assigned in the constructor is serialized with it. Our post on how Laravel prepares a job for the queue covers what the payload looks like once it lands.
So the rule is short: everything a job holds must survive serialize(), sit in a database or Redis for a while, and come back to life in a different process. Some things simply cannot.
What cannot go in the constructor
Closures. They capture scope that PHP cannot represent as a string.
// throws at dispatch
SyncInventory::dispatch($supplier, fn ($item) => $item->sku);
Open connections and resources. A PDO instance, a cURL handle, an open file pointer, a socket. These are live handles to something outside PHP, and there is nothing meaningful to write down.
Service objects that hold those things. This is the one that catches people, because the object looks harmless:
// looks fine, throws at dispatch
public function __construct(
public Supplier $supplier,
public SupplierApiClient $client,
) {}
The client itself might serialize, but if it holds a configured Guzzle instance, a logger with an open stream, or a database connection somewhere down its dependency graph, the serializer walks into it and stops.
Pass data, resolve services in handle()
The fix is the same for all of them. Constructors take plain data. Services are asked for at the point they are used, because Laravel resolves the arguments of handle() out of the container:
class SyncInventory implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(
public Supplier $supplier,
public array $skus,
) {}
public function handle(SupplierApiClient $client): void
{
$client->push($this->supplier, $this->skus);
}
}
The job now carries a supplier id and a list of strings, which is all it ever needed. The client is built fresh in the worker, with whatever configuration that process has.
For a closure, pass the information the closure was going to use instead of the behaviour:
SyncInventory::dispatch($supplier, $items->pluck('sku')->all());
If you genuinely want behaviour to vary, pass a class name or a short string key and map it inside handle(). Those serialize perfectly.
Serializing is not the only reason to keep it small
Even when a large object does serialize, putting it in a job is rarely a good idea. Every byte is written to the queue, read back, and unserialized on every attempt. A job that carries a whole collection of hydrated models when it needed three ids is slower and more fragile than one that does not, and on drivers with a payload size limit it is a failure waiting to happen.
Pass identifiers and let the worker load what it needs. That is exactly what SerializesModels does for Eloquent models, and it is a good instinct for everything else too.
When you need to keep something odd
If a job really must hold an object that will not serialize cleanly, control what gets stored with PHP's own hooks:
public function __sleep(): array
{
return ['supplier', 'skus'];
}
Anything not listed is dropped from the payload and will be null in the worker, so this only works when the object is a convenience you can rebuild. Reach for it rarely. Nine times out of ten the constructor is simply asking for too much.
Wrapping up
If dispatching throws before anything is queued, look at the job's constructor, not its handle(). Closures, live connections, and services that wrap them cannot make the trip. Give the constructor plain data, type-hint the services you need on handle() so the container provides them in the worker, and keep the payload down to identifiers wherever you can.
All comments ()
No comments yet
Be the first to leave a comment on this post.