Background Jobs — Slow Work Off the Request Path
When someone creates a Hoplink, the product team wants three extras: fetch the target page's title for a preview, render a QR code image into R2, and email the owner a confirmation. Together they take several seconds, and any of them can fail. Making the creator stare at a spinner for all that is poor design. The link itself is ready the moment it is saved; the extras can follow.
The rule
Save the essentials, enqueue the extras, return at once.
A background job is a message that describes work to do, plus a consumer that does it:
app.post("/links", async (c) => {
const { code, url } = await c.req.json();
await c.env.DB.prepare("INSERT INTO links (code, url, owner) VALUES (?, ?, ?)")
.bind(code, url, "priya").run(); // essential: now
await c.env.JOBS.sendBatch([ // extras: later
{ body: { type: "fetch-title", code } },
{ body: { type: "render-qr", code } },
{ body: { type: "email-owner", code }, delaySeconds: 60 },
]);
return c.json({ created: code }, 201);
});The consumer switches on type and does each job, acking or retrying as in the previous lesson. delaySeconds holds a message back before its first delivery — here, giving the owner a minute to edit the link before the email goes out.
Keep job messages small: send the code, not the whole page or image. The consumer loads what it needs from D1 or R2 when it runs. Queue messages have a size limit (128 KB), and small messages stay valid even if data changes in between.
Queues or waitUntil? Both run work after the response, but they promise different things:
ctx.waitUntil— runs in the same invocation, with no retries. If it fails or times out, the work is simply lost. Fine for "nice to have" things like a log line.- A queue — the message is stored durably, retried on failure, and processed in a separate invocation with its own time budget. Right for work that must eventually happen.
The shortcut
Sort each step of a request into three bins: needed for the reply → do it now; must happen but not now → queue; nice if it happens → waitUntil.
Save the link
The reply says "created", so it must be true first. Do it now.
Trap: putting the essential step in the queue. If the reply says "created" but the insert is still waiting in a queue, the creator may visit their new link and get a 404.
Drills
Answer fast — trust the rule.
Which step of creating a link must finish before replying "201 Created"?
What does delaySeconds: 60 on a message do?
Why send { type: "render-qr", code } instead of the finished QR image?
Rendering the QR code sometimes fails and must be retried. Why is a queue better than ctx.waitUntil here?
Remember
- Save the essentials, enqueue the extras, return at once.
- Shortcut: needed now → do it; must happen later → queue; nice to have →
waitUntil. - Trap: queuing the essential step, or
waitUntilfor work that must not be lost.
