Skip to content

Laravel error

Laravel: Serialization of 'Closure' is not allowed

The error

Serialization of 'Closure' is not allowed

The queue writes your job to a row or a Redis key before a worker reads it back. A closure cannot survive that trip, and the exception names the closure rather than the property holding it.

The error

Exception

Serialization of 'Closure' is not allowed

Usually thrown from Illuminate\Queue\Queue::createObjectPayload, and usually at the moment you dispatch rather than while the job is running.

What it means

Dispatching a job does not run it. It writes it down - into a database row, a Redis key, an SQS message - so a separate process can read it back later and reconstruct the object. That writing down is PHP's serialize(), and serialize() cannot represent a closure. A closure is compiled code plus a bound scope; there is no text form that survives a round trip through a database column.

So the exception is not about the queue being fussy. It is the queue telling you that part of your job has no way to exist outside this process.

Finding it

The message names the type, not the property, which is why this error eats afternoons. Reproduce it where the stack trace is useful:

it('is serializable', function () {
    serialize(new ProcessOrder($order, $options));
})->throwsNoExceptions();

The failure now points at the line that built the object rather than at the framework internals, and it fails in CI instead of in production.

The usual sources

A closure passed as a constructor argument. The obvious case, and the rarest, because it is visible at the call site.

An object holding a closure. The common one. A configuration object with a formatter callback, a query builder captured before dispatch, a PDF or Excel exporter configured with a column mapper - none of them look like closures from where you are standing.

// the closure is two levels down from the dispatch
$report = new ReportDefinition(columns: [
    'total' => fn ($row) => $row->total / 100,
]);
 
ProcessReport::dispatch($report);

A model with a closure attached. Anything registered on an instance rather than on the class - a local scope bound at runtime, a relation resolved with a callback - rides along when the model is serialized.

A container-resolved dependency. Injecting a service into a job's constructor serializes that service and everything it holds, which can be a whole graph including a client configured with a retry callback. Resolve it inside handle() instead.

The fixes, in the order worth trying

Pass data, not behaviour. Send the arguments the closure needed and put the closure back on the other side. A job's constructor should take scalars and models; anything else deserves a moment's thought.

// before
ProcessReport::dispatch(new ReportDefinition(columns: [
    'total' => fn ($row) => $row->total / 100,
]));
 
// after
ProcessReport::dispatch(reportId: $report->id, format: 'minor-units');

Name the behaviour. Where the callback really is the point, replace it with a class name - a string that serializes fine and that you resolve when the job runs:

ProcessReport::dispatch($reportId, formatter: MinorUnitsFormatter::class);

Rebuild it in handle(). Dependencies that come from the container do not need to travel:

public function handle(PdfRenderer $renderer): void
{
    // resolved on the worker, never serialized
}

Drop the property, deliberately. __sleep(), or a #[\AllowDynamicProperties]-free explicit list, will exclude it - but the property is null on the worker, and that is a decision to write down rather than to discover.

The rule that prevents it

A queued job is a message, not a callback. If you can write the job's constructor arguments into a JSON object without losing anything, the queue can carry it. If you cannot, the queue is telling you something true about the design rather than being difficult.

A job that cannot be serialised and a job that runs twice teach the same thing from two sides: the queue carries a message, and a message can be delivered again. If the whole queue layer is what you cannot trust, we take it on as an engagement.

Related questions

Queue::push with a closure works, though.
It does, because Laravel uses a library that compiles the closure's source and its bound variables into something storable. That covers a closure you write directly at the dispatch site. It does not cover a closure that arrives as a property of an object you passed in, which is where this exception nearly always comes from.
Can I just mark the property as not serialized?
You can, and occasionally it is right - a logger or a client that the job can rebuild from configuration is a fair candidate. Be clear about what you are agreeing to: the property will be null when the job runs, so any code path that touches it has to cope with that, on a worker, without you watching.
Why did it only fail on the second attempt?
A job is serialized once at dispatch, then again when it is released back to the queue. A retry path that adds state to the job - a callback registered during the first attempt, a cached client - can leave something unserializable on an object that dispatched cleanly.
How do I find the offending property?
Call serialize() on the job at the dispatch site and let it throw there, with a stack trace pointing at your code rather than at the framework's queue layer. Three lines in a test will tell you what an hour of reading the exception will not.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page