September 12, 2026
QUEUES TURN A TRAFFIC JAM INTO AN ORDERLY LINE

Cron (26.2) starts work on time. But some work does not arrive on time — it arrives all at once.
The 500-upload burst
Launch day. Five hundred users upload documents within the same hour, and each upload needs text extraction, chunking, and embeddings before the RAG search works. The naive design starts all of it immediately: 500 concurrent model calls hammer the provider, rate limits bite, the app runs out of memory, and half the uploads fail in ways nobody can reconstruct.
The problem is not the total work. It is the *simultaneity*. The fix is a line with memory:
- A queue is a durable waiting line of job messages. It remembers every request even if the app restarts.
- A worker is the program that takes one message and does the work — outside any user's request (26.1's background job, now with a formal waiting room).
Instead of 500 simultaneous sprints, you get an orderly line processed at a pace your provider, database, and budget can sustain.
The four parts, always in this order
Every queue system — regardless of vendor — has the same four components:
Producer → Queue → Worker → Database (outcome)
| | | |
| | | └─ job status + result the user can see
| | └─ takes one message, does the work, reports back
| └─ durable waiting line; survives restarts
└─ anything that creates work: request handler, cron trigger, webhook
1. Producer adds a small job message ("process document 847") — it does *not* do the work. 2. Queue holds the message durably until someone processes it. 3. Worker picks up one message (or a controlled batch), performs the action, and records the outcome. 4. Database records the outcome: status, result location, error if any. This is the same job-record habit from 26.1, now fed by a line instead of a single handoff.
As a current example, Cloudflare Queues shows the pattern concretely: producers publish messages, Queues buffers them, consumer Workers process them with retries and controlled concurrency. The product name will age; the four-part shape will not. Any managed queue you meet later — and there are many — maps onto the same diagram.
Delivery realities: late, repeated, duplicated
A queue is durable, not magical. Three things will happen to your messages, and your design must expect all of them:
- Delayed: a message waits minutes or hours during a backlog. Anything assuming "processed within seconds" breaks.
- Retried: a worker crashes mid-job, so the message returns to the line and runs again.
- Duplicated: the same message can be delivered more than once. At-least-once delivery is the norm, exactly-once is the fantasy.
That is why jobs need identifiers and safe-repeat behavior: each job carries a stable ID, and processing the same ID twice produces one correct result, not two. Lesson 26.4 teaches this fully under the name *idempotency*. For now, remember the rule: never design a worker that corrupts data if it sees the same message twice.
Worked example: 100 documents for RAG
A knowledge-base feature accepts 100 PDFs for a RAG collection. Here is the queue version:
Producer (request handler, fast): for each upload, save the raw file to object storage, insert a documents row (id, owner, storage_path, status: queued), and publish one message per document: { "job_id": "doc-847-embed", "document_id": 847 }. The user sees "100 files received — processing" in under a second.
Queue: holds 100 messages durably. If the app deploys mid-burst, the messages wait. Nothing is lost.
Workers (controlled parallelism): two to four workers each take one message at a time: extract text, split chunks, create embeddings, write chunk records linked to the document, flip the document row to ready (or failed with the failed step named). Concurrency is a dial — turn it up for speed, down when the embedding provider throttles.
Database (outcome): the documents table is the user-visible truth. The collection page reads it: "73/100 ready," each row linking to its chunks. A failed row shows *which step* failed, so a retry resumes instead of restarting from zero.
Notice what the queue bought: uploads never time out, provider rate limits are respected, failures are per-document instead of per-batch, and the user watches honest progress instead of a frozen spinner.
A forward link worth planting now: later, agent actions will need this same pattern. Anything long-running or costly — a multi-step agent task, a batch of model calls — belongs behind a queue with IDs, status records, and retries, not fired directly from a button.
Exercise: draw a queue plan
Pick one bursty feature: file upload, daily research generation, or image generation. Write a five-line plan:
## Queue plan: <feature>
- Producer: <what creates the message — handler, cron, webhook?>
- Job payload: <exact fields, including the stable job ID>
- Worker action: <ordered steps the worker performs>
- Result record: <table, key, and statuses the user sees>
- Retry condition: <what failure retries, what failure stops and asks a human>
Example payload to imitate: { "job_id": "report-2026-09-12-acme", "report_id": 42, "attempt": 1 } — small, stable, and re-runnable.
Finish line: a queue plan with all five lines filled and a payload containing a stable ID.
Verification: simulate a crash — "the worker died after step 2 of 4." Using only your plan, answer: does the message return to the queue, what does the user see, and does the retry duplicate anything? If any answer is "unknown," the plan needs the missing line.
Common failure mode: a payload containing the whole file or the whole report instead of an ID. Queues carry *references* (IDs, paths); the heavy bytes live in storage and the database. Fat messages clog the line they were built to smooth.
Check your understanding
- Why does starting 500 embedding jobs immediately fail even though each job is correct in isolation?
- What does the database record in the four-part pattern, and who reads it?
- A message arrives twice. Which design choice decides whether that is harmless or corrupting?
Next
Queues make bursts survivable — but only if repeats are safe. Lesson 26.4 goes deeper on the difference between "ran" and "worked": retries, timeouts, duplicates, dead letters, and the idempotency habit that keeps them honest.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
