Blog
If It Can Wait, Put It In a Queue
- software-engineering
- laravel
- architecture
A lot of “slow app” problems are really “we did too much work in the request” problems.
The user clicks a button. Your code sends an email, rebuilds a report, hits three third-party APIs, resizes an image, and writes a pile of audit rows. Somewhere in there the browser spinner starts to feel personal.
If the user doesn’t need the result before the next screen loads, that work does not belong in the request.
Put it in a queue.
What a queue is for
A queue is a waiting line for work your app can finish later.
The request does the small, urgent part: validate input, save the important record, return a clear response. Then it hands the slow or flaky parts to a job. A worker picks that job up when it can. Retries happen without making a person stare at a loading state.
That’s it. Not magic. Not a rewrite. Just separating “answer the human” from “finish the chores.”
The stuff that almost always can wait
Email and notifications. PDF or CSV exports. Image processing. Cache warmups. Search index updates. Webhook fan-out. “Notify three other systems that this order changed.” Nightly cleanup that someone accidentally wired to a button.
If the product copy could honestly say “we’ll email you when it’s ready,” you already have your answer.
Same idea inside admin tools. An export that takes forty seconds is fine as a job. It is rude as a synchronous page load.
Why doing it in the request feels easier
Because it is, the first time.
One function. One stack trace. No worker process to run locally. No failed jobs table to learn. You ship the feature and move on.
Then traffic grows. The email provider hiccups. The image library spikes CPU. A third-party API starts timing out on Tuesdays. Now your checkout, your contact form, or your “save and continue” flow fails for reasons that have nothing to do with the user’s data.
You didn’t build a product path. You built a Rube Goldberg machine that only works when every dependency is in a good mood.
Queues make failure boring
Failed jobs are supposed to happen.
Networks flake. Rate limits exist. Remote APIs reboot. A good queue setup gives you retries, backoff, and a place to inspect what blew up without making the user re-click and hope.
That is a quieter kind of reliability than “add more timeouts to the controller and pray.”
You also get natural batching. Ten password-reset emails shouldn’t mean ten identical chunks of request-time work fighting for the same PHP workers. Push ten jobs. Let the queue spread the load.
Keep the job small and honest
A queue is not a junk drawer for bad design.
Name the job after the outcome: SendOrderConfirmation, RebuildUserExport, SyncCustomerToCrm. Pass the IDs you need, not a giant mystery payload you can’t replay. Make the job idempotent when you can, so a retry doesn’t double-charge, double-email, or double-create a remote record.
If the job needs twenty services and a prayer, the problem isn’t the queue. It’s the boundary.
Laravel makes this normal on purpose
This is one reason I still like Laravel for app work.
Jobs, queues, failed jobs, retries, and ShouldQueue are first-class. You can start with the database driver when that’s enough, then move to Redis when volume asks for it. The habit stays the same either way: decide what must finish before the response, and what can finish after.
WordPress and plain PHP can do the same idea with Action Scheduler, a real queue, or even a carefully owned cron plus a jobs table. The tool matters less than the split.
When you should not queue it
Don’t queue the thing the next screen depends on.
If the user needs the new record ID, the updated total, or a validation error before they continue, do that work now. Don’t hide business rules in a job that might run in thirty seconds. Don’t use a queue to postpone a decision you haven’t figured out yet.
And don’t pretend a queue fixes a slow query forever. It can hide the symptom from the browser while the database still suffers. Profile that separately.
The habit
Before you ship a new write path, ask one question:
Does the user need this result before the response comes back?
If yes, keep it in the request and make it fast. If no, put it in a queue, give the user an honest “we’re on it,” and let the workers do the boring work.
Fast apps are often just apps that stopped making people wait for chores.