This page targets Laravel 13 and checks the implementation of Framework
v13.42.0. It assumes queue basics and package testing. The compatibility design below is a package-side policy. Laravel has no feature that automatically migrates your own payload formats.Code changes between storing and running
For object jobs,Queue::createObjectPayload() runs serialize(clone $job) and puts the result in the payload’s data.command. When encryption is enabled, the same serialization happens before it.
On the worker side, CallQueuedHandler::getCommand() runs unserialize() on the stored data and passes the restored job to the Dispatcher. A regular handle() runs with the worker’s code and service container as they are at that moment.
If you do not restart long-running workers, the old code keeps running after the update. With rolling deployments, consider not only “old dispatcher with new worker” but also “new dispatcher with old worker”.
Do not judge renaming or removing a class only by whether users call that class directly. Jobs that are waiting, delayed or awaiting retry, and the retention period of failed jobs, are also part of compatibility.
What SerializesModels stores
Illuminate\Foundation\Queue\Queueable uses SerializesModels internally. Besides replacing models, it provides __serialize(), which lists the job’s own properties, and __unserialize(), which puts the values back.
Values equal to the declared default are not stored
__serialize() excludes static and uninitialized properties, and it also skips storing a property when its current value is strictly equal to the property’s declared default.
'csv', format is not in the stored array. If a later release changes the declared default to 'json', 'json' may also apply to old jobs. This is not a cosmetic default change: it changes how old data is read.
The constructor is not called again on restore
__unserialize() assigns values to properties that exist on the current class and have a key in the stored array. It does not run any filling or validation of input through the constructor.
'csv' in this example is the default of a constructor argument. It is not the property’s own declared default, so it does not fill in the value when you restore an old payload without format. If an added typed property stays uninitialized, accessing it later fails.
Also, a public property’s key is its name, a protected property’s key has a prefix for its visibility, and a private property’s key also includes the class name. Verify a change from public to private with old payloads too.
Separate identifiers from execution services
Store the identifiers and options needed for execution in the job, and inject execution services such as API clients and exporters intohandle(). If you pass a service to the constructor and keep it, its internal state may become part of what is stored.
The next example adds a format choice and a payload version to a job with the same name that held only reportId in the old version. ReportExporter is a contract the package provides and binds in the container, with export(string $reportId, string $format): void.
- If an old payload has no
format, the declared default'csv'is used. - If an old payload has no
payloadVersion, it is treated as the declared default1. - For newly created jobs, the constructor sets
payloadVersionto2. Because it differs from the declared default, this value is stored. - If a new job’s
formatis'json', that value is stored too.'csv'is skipped, so keep the declared default'csv'from now on.
Distinguish passing models from snapshots
When you store a model directly in a property,SerializesAndRestoresModelIdentifiers replaces it with a ModelIdentifier holding the class, ID, connection name, names of loaded relations and so on. When restoring a single model, it re-retrieves it from the database with firstOrFail() and loads the target relations.
This is not a mechanism that freezes attributes or related data as of dispatch time.
- If the record is updated before execution, the restored model reflects the updated content.
- Constraints applied to relations beforehand are not stored, so reapply the conditions you need at execution time.
- Exclude unneeded relations from storage with
withoutRelations()or#[WithoutRelations]. - If the model is deleted, the job can fail during the restore step, before the job’s
handle().
#[DeleteWhenMissingModels]. However, the package should not make discarding the default on its own; expose the failure policy to users. If you pass only an ID and look it up in handle(), you control the failure or skipping of that lookup yourself.
If you need the content as of dispatch time, store the values you need explicitly, or record something like an immutable report request in the database and pass its ID. Distinguish simply passing a model from storing a snapshot of business data.
Dispatching after commit is a separate problem
When you process records created inside a transaction, make sure workers do not start reading them before the commit.afterCommit() waits to dispatch until the open parent transaction commits, and discards the dispatch on rollback. It does not solve class name and property compatibility or duplicate execution of external processing.
Separate fakes from old payload tests
A regularQueue::fake() records dispatches. A passing assertPushed() alone does not confirm storing and restoring, or that the new version processes old payloads.
You can verify a round trip with the current class like this.
serializeAndRestore() runs serialize() and unserialize() inside the fake. However, it does not verify dispatching to a real backend, execution by a worker, or generation by the old code.
Keep fixtures generated by the old version
While the oldGenerateReport has only reportId, generate a fixed job that contains no real data. The following is preparation code you run once in the old version’s test environment. Create tests/fixtures/ beforehand.
TestCase in this example is the base class you set up in the package testing environment. Only pass fixtures you manage yourself to unserialize(), never external input.
Also run the restored old job’s handle() with a test exporter and verify its output and stored results. When you pass models, separately verify updates, deletion, connections and re-retrieval of relations, and, on the queue connection you actually use, dispatching after commit and retries.
Update the reading side before dispatching the new format
1
Prepare a release that can read the old format
First ship code that keeps the old class name and the interpretation of old values and can also read the new format. Use fixtures to check not only restoring but also processing results.
2
Confirm that no old workers remain
Include replacing workers in the consuming app’s deployment procedure. With a regular
queue:work, php artisan queue:restart asks workers to exit after their current job, and you confirm that the process manager starts the new version. The cache is used for the restart signal.3
Enable dispatching the new format
Once the reading side supports it, switch to dispatching the new format, for example with a feature flag. If shipping the same code also starts generating the new format, you need a procedure that keeps it away from old workers, such as pausing dispatch or separating old and new queues with matching workers.
4
Maintain support through the retry and rollback period
Also check retries of failed jobs and jobs dispatched later within chains. If you roll back to old code after storing the new format, verify that the old code can read it. Do not remove support for old classes or old formats just because the queue was empty once.
queue:restart is not a command that rewrites stored payloads. In the package’s upgrade notes, state the supported formats, the required worker replacement and the conditions for retiring the old format. Record the same change in version compatibility management and changelog practices.
Primary sources
- Laravel docs: Class structure and dependency injection
- Laravel docs: Jobs and database transactions
- Laravel docs: Queue workers and deployment
- Laravel docs: Ignoring missing models
- Queue: Creating object payloads
- Queueable: Composition of the job trait
- SerializesModels: Storing and restoring properties
- SerializesAndRestoresModelIdentifiers: Replacing and re-retrieving models
- CallQueuedHandler: Restoring and running
- QueueFake: Storing and restoring inside the fake
- Laravel framework tests: Model serialization