Queues
By the end of this lesson you'll be able to push slow work — emails, image processing, API calls — onto a background queue so your app responds instantly, and run a worker that retries, backs off, and never loses a job.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Why Offload Work to a Queue?
When a request comes in, your code runs top to bottom and only then sends a response. If that code blocks — sits and waits — on something slow like sending email (2–3 seconds talking to a mail server), resizing an image, or calling a third-party API, the user stares at a spinner the whole time. A queue fixes this: instead of doing the slow work now, you write a tiny note describing the work (a job ), drop it on the queue, and reply immediately. Something else does the work later. Here is the difference side by side.
The blocking version makes the user wait for the email; the queued version returns the moment the job is on the queue. The email still gets sent — just a moment later, by a separate process. The user's experience goes from slow to instant .
2️⃣ Producers, Consumers, Workers & Brokers
Four words describe every queue system. The producer is the code that creates a job and pushes it (usually your web app). The broker is the storage that holds jobs in the middle — it's the ticket rail. The consumer (also called a worker ) is a long-running process that pulls jobs off the broker and runs them. Producers and workers don't talk to each other directly; they only ever touch the broker, which is what lets them scale and fail independently.
You have several choices of broker, and they trade simplicity for power:
Most PHP apps start with Redis or a database queue and only move to RabbitMQ when they genuinely need its routing. The concepts below are identical whichever you pick.
3️⃣ The Job: a Unit of Deferred Work
A job is a small object that describes work to do later. Because the producer and worker run in different processes — often on different machines — the job has to be serialisable : turned into plain text (usually JSON) to store in the broker, then rebuilt on the worker side. The golden rule is keep the payload tiny : store a user's id , not the whole user object. The worker reloads fresh data when it runs, so the job stays small and never goes stale.
Notice the round trip: toJson() produces the string that lives in the broker, and fromJson() rebuilds the job on the worker so it can call handle() . That handle method is where your real work goes.
4️⃣ Your Turn: Make a Producer Respond Instantly
Now you try. The upload handler below should queue the resize work and return straight away, never making the user wait for the image to be processed. Fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
5️⃣ The Worker Loop: Retries, Backoff & Dead-Letter
A worker is a loop: pull a job, run it, and decide what to do with the result. Real work fails for boring, temporary reasons — a network blip, a rate-limited API — so a worker retries . To avoid hammering a struggling service, each retry waits a little longer than the last; this is exponential backoff (1s, then 2s, then 4s…). When a job has used up all its attempts, you don't throw it away — you move it to a dead-letter queue , a separate list where a human can inspect what went wrong. And because a worker can crash after doing the work but before recording it, jobs must be idempotent : running one twice has the same effect as running it once. The example below shows all four working together — fully deterministic, so you can match the output exactly.
Trace the charge_card job: it fails twice, so the worker re-queues it with growing backoff (1s, then 2s), and succeeds on the third attempt. The idempotency guard means that even if that job had been delivered twice, the second delivery would print skip rather than charging the card again. Nothing reaches the dead-letter queue here because every job eventually succeeds.
6️⃣ Your Turn: Finish the Retry Rule
Here's the heart of a worker loop with one piece missing: the rule that decides when to stop retrying and send a job to the dead-letter queue. Fill in the ___ with the right comparison operator, then run it.
7️⃣ A Real Broker: Redis + a Worker Process
Swap the in-memory array for Redis and the same lifecycle becomes durable across processes and restarts. The producer uses LPUSH to add a job; the worker uses BRPOP , which blocks (waits efficiently, using no CPU) until a job arrives or it times out. The idempotency guard here is SETNX — "set if not exists" — which returns false if this job id was already handled, so duplicate deliveries are skipped safely.
That while (true) loop must stay alive forever, restart if it crashes, and not be killed mid-job. You don't babysit it by hand — a process manager like Supervisor does. This config runs three parallel workers and restarts any that die.
8️⃣ In Practice: Laravel Queues & Symfony Messenger
You've now built every piece by hand so you understand it — but in production you let your framework do the plumbing. In Laravel , a job is a class implementing ShouldQueue ; you dispatch it with SendWelcomeEmail::dispatch($user->id) and run workers with php artisan queue:work . Retries, backoff, delays, rate limiting, batching and a failed_jobs table all come built in across Redis, SQS, Beanstalkd and database drivers. Symfony Messenger follows the same shape: a message class, a handler, configurable transports (the broker), and retry/failure strategies in config. The vocabulary you learned — producer, broker, worker, retry, dead-letter, idempotency — maps straight onto both.
📋 Quick Reference — Queues
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the same write-run-check loop you'll use on every real worker.
Practice quiz
Why offload slow work like sending email to a queue?
- It makes the email arrive faster
- It encrypts the email
- The request can respond instantly instead of blocking on the slow work
- It removes the need for a database
Answer: The request can respond instantly instead of blocking on the slow work. Instead of blocking 2-3 seconds on the mail server, you push a tiny job and reply immediately; a worker sends it later.
In queue vocabulary, what is the broker?
- The storage that holds jobs in the middle (e.g. Redis, RabbitMQ)
- The code that creates and pushes a job
- The long-running process that runs jobs
- The dead-letter list
Answer: The storage that holds jobs in the middle (e.g. Redis, RabbitMQ). The producer pushes jobs, the worker pulls them, and the broker is the storage between them — they only ever touch the broker.
What should a job's payload contain?
- The whole user object serialised
- The database connection
- The worker's source code
- A tiny payload such as an id, not the full object
Answer: A tiny payload such as an id, not the full object. Keep payloads tiny — store an id, not the whole object — so the job survives JSON serialisation and the worker reloads fresh data.
What is exponential backoff in a worker's retry logic?
- Retrying instantly every time
- Waiting longer before each retry (1s, then 2s, then 4s...)
- Doubling the payload size each retry
- Skipping every other retry
Answer: Waiting longer before each retry (1s, then 2s, then 4s...). Each retry waits longer than the last (1s, 2s, 4s...) so a struggling service isn't hammered while it recovers.
Where does a job go after it has exhausted all its retry attempts?
- A dead-letter queue for a human to inspect
- It is silently deleted
- Back to the front of the main queue forever
- The producer's memory
Answer: A dead-letter queue for a human to inspect. Exhausted jobs move to a dead-letter queue — a separate list — instead of being lost, so a human can investigate.
What does it mean for a job to be idempotent?
- It can only run once ever
- It runs in parallel
- Running it twice has the same effect as running it once
- It never fails
Answer: Running it twice has the same effect as running it once. Because brokers guarantee at-least-once delivery, jobs must be idempotent — a stable id you record on success makes a re-delivery harmless.
Which Redis command does the worker use to wait efficiently for a job without busy-looping?
- LPUSH
- BRPOP
- SETNX
- EXPIRE
Answer: BRPOP. BRPOP blocks (waiting efficiently, using no CPU) until a job arrives or it times out, so the worker doesn't busy-loop.
Which Redis command provides the idempotency guard in the lesson's worker?
- LPUSH — returns false on a duplicate
- INCR — counts deliveries
- DEL — removes duplicates
- SETNX — returns false if the id was already processed
Answer: SETNX — returns false if the id was already processed. SETNX (set if not exists) returns false when the id was already handled, so a duplicate delivery is skipped safely.
Why must long-lived PHP workers be restarted periodically?
- To pick up a new IP address
- Small memory leaks accumulate over hours/days until the process is killed
- Redis forgets the connection
- PHP requires it every 60 seconds
Answer: Small memory leaks accumulate over hours/days until the process is killed. Unlike a normal request that's torn down at the end, a worker runs for hours; restart it (--max-jobs/--max-time) so leaks don't grow unbounded.
What keeps worker processes alive and restarts them if they crash?
- The web server
- The Redis broker
- A process manager like Supervisor
- The dead-letter queue
Answer: A process manager like Supervisor. Supervisor (with autorestart=true) keeps the while(true) workers running and restarts any that die.