A support ticket lets people attach a screenshot, and you want the resizing done in the background. The obvious thing fails immediately:

ProcessAttachment::dispatch($request->file('screenshot'));
Exception: Serialization of 'SplFileInfo' is not allowed

There are actually two separate problems here, and the error only tells you about the first one.

Why it cannot be serialized

An uploaded file object is a thin wrapper around a path on disk, and PHP refuses to serialize SplFileInfo and its descendants. There is nothing meaningful to write into a payload, so it throws at dispatch.

The problem the error does not mention

Even if it serialized perfectly, it would not work. PHP stores uploads in a temporary location and deletes them when the request finishes. Your worker picks the job up seconds or minutes later, by which time the path in the payload points at nothing.

This is the part that catches people who work around the serialization error by passing $file->getPathname() as a string. That dispatches happily and then fails in the worker with a file-not-found, usually only under load, which is a miserable bug to chase.

Store it first, queue the path

Move the file somewhere permanent during the request, and give the job the location:

public function store(Request $request)
{
    $path = $request->file('screenshot')->store('attachments', 's3');

    ProcessAttachment::dispatch($path);

    return back();
}
class ProcessAttachment implements ShouldQueue
{
    use Queueable;

    public function __construct(public string $path) {}

    public function handle(): void
    {
        $contents = Storage::disk('s3')->get($this->path);

        // resize, scan, whatever the work is
    }
}

The payload is now a short string. The file exists independently of the request, and the job can be retried tomorrow and still find it.

The worker has to be able to reach it

Storing on the local disk works fine until you run workers on a second machine. The web server writes to its own filesystem, the worker looks on its own filesystem, and the file is not there.

If web and workers are separate machines, put the file on shared storage: S3 or equivalent. This is one of the most common causes of a job that works perfectly in staging on one box and fails in production on three.

Cleaning up without breaking retries

Temporary uploads should not accumulate forever, but deleting them in the wrong place turns a retryable failure into a permanent one.

Do not delete in a catch inside handle(). That runs on the first failure, and the retry then arrives to find the file gone, so a transient error becomes fatal:

// wrong: kills every subsequent attempt
try {
    $this->process();
} catch (Throwable $e) {
    Storage::disk('s3')->delete($this->path);
    throw $e;
}

Delete on success, and on final failure only:

public function handle(): void
{
    $this->process();

    Storage::disk('s3')->delete($this->path);
}

public function failed(?Throwable $e): void
{
    Storage::disk('s3')->delete($this->path);
}

failed() runs once, after the last attempt, so the file survives every retry and is tidied up when the job genuinely gives up. That timing is worth understanding properly.

For anything that slips through both paths, a scheduled sweep of old temporary files is a sensible backstop.

Do not pass the contents instead

Reading the file and passing the bytes technically serializes, and it is a bad trade. A two megabyte payload is written to the queue, read back, and unserialized on every attempt, and on SQS it will not fit at all. Keep the payload down to identifiers and let the worker fetch what it needs.

Wrapping up

An uploaded file cannot go in a payload, and the temporary file would not survive the wait even if it could. Store the upload during the request, dispatch the path, keep it on storage every worker can reach, and delete it on success or in failed() so retries always still have something to work with.

Related