You moved a slow thumbnail job onto the queue, deployed, and the endpoint is exactly as slow as it was before. There are no rows in the jobs table. Worse, when the job throws, the user sees the error in their browser rather than it landing quietly in failed_jobs.

Nothing is broken. Your queue connection is sync.

What sync actually does

The sync driver is a queue connection that does not queue. When a job is dispatched it runs it immediately, in the same process, before dispatch() returns:

ProcessThumbnail::dispatch($upload);   // runs right here, right now

And it rethrows. If the job throws, that exception propagates straight into the request that dispatched it. That is why a failing job shows up as a 500 rather than a failed job row: ShouldQueue is on the class, but nothing was ever queued, so there was no attempt to fail.

Check what you are actually on

php artisan tinker --execute="echo config('queue.default');"

If that prints sync, everything above is your answer. In .env:

QUEUE_CONNECTION=database

Fresh Laravel installs now default to database, but plenty of projects still carry sync from an older skeleton, a copied .env.example, or a .env on the server that was never fully filled in.

The one that catches people on deploy

Config caching. If the server ran php artisan config:cache while QUEUE_CONNECTION was unset or wrong, the cached config wins and editing .env afterwards changes nothing at all. You will swear the setting is correct, because it is, and it is being ignored.

php artisan config:clear   # then re-cache once .env is right
php artisan config:cache

Always confirm with config('queue.default') rather than reading the .env file, because that is what the application is actually using.

Sync is not a bug, it is a tool

It exists for good reasons, and it is often the right choice:

  • Local development where you do not want to babysit a worker.
  • Tests where you want the job's effects to have happened by the time the assertion runs.
  • Small deployments with no worker process, where a slightly slower request is an acceptable trade for one less thing to run.

The problem is only ever sync appearing where you expected a real queue.

In tests, prefer being explicit

Relying on the environment to be sync makes tests quietly dependent on config. Say what you mean instead:

Queue::fake();

$this->post('/uploads', [...]);

Queue::assertPushed(ProcessThumbnail::class);

Queue::fake() asserts the job was dispatched without running it. When you do want it to run, Bus::dispatchSync() or calling the job directly is clearer than depending on a connection setting.

The opposite symptom

The mirror image of this problem looks similar from the outside and has the opposite cause: rows pile up in the jobs table and nothing ever happens to them. That is not sync. That is a real queue with no worker consuming it, or a worker consuming a different queue or connection than the one you are dispatching to.

Symptom Cause
Request waits, job runs, no rows in jobs connection is sync
Rows appear in jobs and stay there no worker running, or it is on another queue
Job runs but with old code worker not restarted after deploy

That last one has its own set of causes worth knowing.

Wrapping up

If dispatching a job blocks the request and its exceptions reach the browser, you are on the sync driver. Check config('queue.default') rather than the .env file, clear a stale config cache if they disagree, and keep sync for local work and tests where running inline is genuinely what you want.

Related