Asynchronous foundations
Move work off the request path
Return a fast accepted response when a user does not need a slow side effect to finish synchronously.
Think about a sign-up. You create the account. Then you want to send a welcome email, update analytics, and ping Slack. The user doesn’t care about any of that. They just want to be logged in.
The principle: an HTTP request should wait for the work it needs to decide its response, and nothing else. Slow email, export, indexing, or webhook work can move to an asynchronous system after the authoritative input is stored.
Store first, defer second. A queued job that references an account that was never saved is a bug factory.
You have three deferral tools on Cloudflare. They differ by what survives failure.
waitUntil: best effort
ctx.waitUntil extends one event for short work. The response goes out, and the Worker keeps running a little longer:
export default {
async fetch(request, env, ctx) {
const user = await createUser(request, env)
ctx.waitUntil(logSignupToAnalytics(user.id, env))
return Response.json({ ok: true })
},
}
If that analytics call fails, nothing retries it. Nothing even remembers it. That’s fine for a metrics ping and unacceptable for anything a user was promised.
Queues: durable delivery
A Queue stores the message, delivers it to a consumer, and retries on failure:
await env.EVENTS.send({ type: 'welcome-email', userId: user.id })
return Response.json({ ok: true })
send returns as soon as the message is accepted. The user gets their response right away, and the email work now has retries, even if the email provider is down for ten minutes.
Workflows: durable multi-step execution
When the deferred work is a sequence, like charge, provision, notify, with state that must survive failures between steps, use a Workflow. It records each completed step durably and resumes from the failure point, even across days of waiting.
Duration is not the criterion
Choose from durability and orchestration needs, not from how long the work takes. A two-second task the business cannot lose belongs on a Queue. A fast fire-and-forget ping is fine in waitUntil no matter how busy the system is.
Ask “what happens if this fails halfway?” The answer picks the tool.
Try classifying these four: cache invalidation, welcome email, large export, and a week-long approval process. My answers: waitUntil for the cache purge, since a miss just costs one slower request. A Queue for the welcome email, promised but single-step. A Queue feeding real processing for the export. A Workflow for the approval, because a week of waiting with state is exactly what durable execution is for.
Lesson completed