September 12, 2026
NOT EVERY JOB BELONGS INSIDE A USER REQUEST

Class 25 gave every workload a home — your laptop workshop, a rented VPS computer, a managed platform, or a short-lived serverless function. Now a sharper question: when a user clicks a button, how much work should happen before you answer them?
Too many first products answer "all of it." That is where this lesson starts.
The timeout everyone has felt
Picture a research product built on the ByeBuy pattern. A user clicks Generate report. The server starts working: it fetches filings, pulls macro data, calls a model, writes a PDF, and sends an email. The browser spinner turns. Ten seconds. Thirty seconds. Sixty. Then the request times out, the user refreshes, clicks again — and the server starts the whole expensive chain a second time.
Nothing is architecturally exotic here. The mistake is timing: long, multi-step infrastructure work was stuffed inside a single user request.
Here is the vocabulary for the fix:
- Synchronous means the user waits for the result. Request comes in, work happens, response goes back, and the browser holds its breath the whole time.
- Background job means the work is recorded for later processing while the user gets a fast acknowledgement plus a way to check status. The request says "got it, here is your job, I will tell you when it is done" — and then a separate process does the heavy lifting.
The distinction is not about importance. The report matters enormously. It is about *when the user needs proof of completion* versus *when the system can finish quietly and notify*.
The research-report flow, step by step
Take that same "Generate report" feature and rebuild it as background work:
User clicks "Generate report"
→ app creates a job record (id, user, parameters, status: queued)
→ job enters a queue
→ user immediately sees "Report queued — we will notify you"
→ a worker picks up the job, gathers data, calls the model, saves the report
→ job record flips to "done" (or "failed" with a reason)
→ user sees the finished report or gets a notification
Three things changed. First, the request handler does almost nothing: validate input, create a job record, enqueue, respond. Second, the slow work happens in a worker — a background program that takes a job and performs it outside any user's request. Third, the database holds the truth about the job's state, so a refresh, a timeout, or a second click never restarts the work blindly.
That middle record is the whole trick. Without it you have a hope ("the email probably sent"). With it you have a system: every job has an identifier, a status, and a result location.
What belongs where
Background work is not a rare exception. Most of a real product's value is background work:
| Good background candidates | Why |
|---|---|
| Embeddings and chunking for RAG | Slow, bursty, no user waiting on each chunk |
| Bulk imports and file conversion | Minutes of CPU/file work |
| Email and notifications | Third-party service, can fail and retry |
| Data refresh (nightly filings, macro series) | Nobody clicked; the clock triggered it |
| Long reports and media generation | Model calls, PDFs, images, audio |
| Cleanup (expired sessions, old previews) | Invisible maintenance |
And work that should stay synchronous is work the user cannot proceed without:
| Keep it synchronous | Why |
|---|---|
| Small input validation | Must reject bad input before anything else |
| Immediate search over prepared data | The next click depends on the answer |
| Quick database read of a cached page | Milliseconds, already computed |
A useful test: if the work takes longer than a few seconds, costs real money per run, calls an outside service, or can safely finish after the user leaves the page — it is a background candidate. If the user's next action is blocked until the answer arrives and the answer is fast and cheap — keep it in the request.
The UX rule
Never make someone watch infrastructure work if the product can show progress and let them return. That means every background feature needs three visible pieces:
1. Acknowledgement — "Report queued at 2:14 PM, position 3." 2. Progress or status — a jobs page, a status badge (queued → working → done), or a progress bar. 3. Finish line — the exact user-visible outcome: the report page URL, the download link, the "ready" email. The user should never wonder "did it work?"
A spinner that lasts four minutes is a bug report waiting to happen. A queued state with a finish line is a feature.
Worked example: two paths for one feature
Feature: "Import a CSV of 2,000 companies and flag matches against the watchlist."
Before the response (synchronous, fast):
- Validate the file type and size.
- Save the upload to object storage; create a database record (
importsrow: id, owner, filename, statusqueued). - Enqueue an
import-processjob with the import ID. - Respond: "Import received — 2,000 rows queued. We will email you when matching is done."
Total request time: under a second.
After the response (background, slow):
- Worker picks up the job, reads the CSV in batches, matches rows, writes results to a
matchestable, updates the import row todonewith a count and a results URL. - On failure, the row flips to
failedwith the reason and which batch stopped — never a silent hang.
User-visible finish line: the user opens the import page and sees "2,000 rows processed, 47 matches" plus a link to the matches table, or receives the "your import is ready" email with the same link.
Exercise: split one feature
Pick one feature from your project — a report, an upload, an email digest, a data refresh.
1. Write its two paths: list exactly what happens before the user gets a response (validate, record, enqueue, acknowledge) and what happens after (the worker's steps in order). 2. Define the visible finish line in one sentence: what the user sees, where, and how they know it succeeded.
Finish line: a short doc with two headed lists ("Before response" / "After response") plus one finish-line sentence.
Verification: hand it to someone else (or a fresh AI session) and ask: "If the browser closed right after the acknowledgement, would the work still finish, and where would I see the result?" If they can answer from your doc alone, the split is correct.
Common failure mode: putting "call the model" or "send the email" in the before-response list. If it can take seconds, fail independently, or cost money — move it after.
Check your understanding
- Why does stuffing a report, an email, and a file write into one request cause duplicate work?
- What three things must the request handler do before responding when work goes to the background?
- Name one feature in your project that should stay synchronous, and why the user is genuinely blocked without it.
Next
You now split work by timing: fast acknowledgement now, heavy work later. Lesson 26.2 puts repeating work on a clock — cron, the alarm that starts jobs whether or not any user is awake.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
