Once you have a queue running, everything starts to look like a job. Slow endpoint? Queue it. Sending an email? Queue it. Updating a counter? Queue it. It feels like the responsible choice, and often it is.
But a queue is not free performance. It is a trade: you exchange immediate certainty for eventual completion, and sometimes that trade is a bad one.
What you actually pay
Moving work off the request costs you four things, and none of them show up in a benchmark:
- Latency to completion. The response is faster; the work finishes later, sometimes much later if the queue is backed up.
- A harder failure story. An inline exception surfaces to the user immediately. A queued failure lands in a table nobody is watching unless you made it tell somebody.
- Serialization limits. Everything the job needs has to survive a trip through the payload, which rules out quite a few things.
- Time travel. The world can change between dispatch and execution, which is a whole class of bug on its own.
If the work is trivial, you can pay all four and gain nothing measurable.
Queue it when
The honest test is whether one of these is true:
- The work is genuinely slow, roughly a hundred milliseconds or more.
- It calls something you do not control, so it can hang or fail independently of your app.
- It should survive a failure and be retried rather than lost.
- It can be parallelised across workers to finish sooner.
- It needs to be rate limited or ordered separately from user traffic.
Image processing, third-party API calls, bulk imports, anything touching a mail server: queue them without hesitation.
Do it inline when
The work is faster than the queue overhead. Writing a row, bumping a counter, clearing a cache key. Dispatching costs a serialize, a write to Redis or the database, and a later read. For a two-millisecond update you have made the system slower overall and added a moving part.
The user needs the result on the next screen. Queue a job to generate something and redirect to a page that displays it, and you have built a race you will lose on a busy queue. Either do it inline or design the page to handle "not ready yet" properly. Half-measures here produce the worst kind of bug report: it works on staging.
A failure must block the operation. If the work must not silently not-happen, and there is no path to notice, it belongs inline where an exception stops the request.
You are queueing to hide a slow query. A job that takes eight seconds because of a missing index takes eight seconds in the worker too. You have moved the symptom off the request and left the cause. Fix the query.
The middle option people forget
Sometimes you want the response out fast but do not need durability, retries, or a worker. dispatchAfterResponse() runs the work in the same PHP process after the response has been sent:
ProcessThumbnail::dispatchAfterResponse($upload);
The user gets their response immediately, and the work happens straight away with no queue involved. The trade is that it dies with the process: no retry, no failed_jobs, nothing survives a crash. That makes it right for genuinely optional work such as logging or a cache warm, and wrong for anything you would be upset to lose.
A quick sanity check
Before adding ShouldQueue, ask what happens if this job runs ten minutes late. If the answer is "nothing much", queue it. If the answer is "the user is staring at a broken page" or "we would double charge somebody", the queue is not the problem you are solving.
Wrapping up
Queues are for work that is slow, risky, or better done in parallel. They are not a general-purpose speed-up, and reaching for one on trivial work buys latency and debugging pain for no gain. Move the heavy and the unreliable off the request, keep the fast and the immediate where they are, and use dispatchAfterResponse() for the small stuff in between.
All comments ()
No comments yet
Be the first to leave a comment on this post.