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.
8 minute lesson
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 await work required 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, and they differ by what survives failure.
ctx.waitUntil extends one event for short best-effort work. The response goes out, 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 provide durable message delivery. The message is stored, delivered to a consumer, and retried on failure:
await env.EVENTS.send({ type: 'welcome-email', userId: user.id })
return Response.json({ ok: true })
The send returns as soon as the message is accepted, the user gets their response immediately, and the email work now has retries even if the email provider is down for ten minutes.
Workflows persist multi-step execution. When the deferred work is a sequence — charge, provision, notify — with state that must survive failures between steps, a Workflow 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 only duration. 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.
Classify these four: cache invalidation, welcome email, large export, and 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 into 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