Async inside one call
db is synchronous: a read or write returns before the next line runs, with no
event-loop hop. fetch, timers and promises you create are ordinary JavaScript:
await them, chain .then, or return a Promise from an async handler.
queueMicrotask runs on the engine's own microtask queue, as in a browser.
export default {
async fetch(request, env, ctx) {
const upstream = await fetch("https://api.example.com/data");
db.events.insert({ at: Date.now() });
ctx.waitUntil(fetch("https://hooks.example.com/seen", { method: "POST" }));
return Response.json(await upstream.json());
},
};
ctx.waitUntil(promise) is not a background task: every waitUntil promise is
awaited after your handler returns and before the response is sent to the client. setTimeout and setInterval answer a numeric id for
clearTimeout/clearInterval; a call's timers run on its own CPU and wall
budget and are cleared when the call ends, so a timer never fires after its
call has finished.
Calls run concurrently
Each HTTP request, cron run, queue delivery or direct function call is its own
call, and many calls of the same app run at once: 512 by default, or the app's
own cf_concurrent_limit (GET /v1/_limits reads it). A call past that limit is
refused with HTTP 429. A call queued past the worker's own queue depth is
refused with HTTP 503. See Limits for every related limit
and error.
State is not shared safely
Module-level variables and globalThis persist on the worker thread that
happens to run a call, not per call: see State between calls.
Treat anything outside db, kv or records as a cache that may be stale, empty,
or shared with a concurrent call you did not expect.
One object, one call at a time
A durable object is the opposite of the model above: its calls queue on a mailbox and run strictly one at a time, so its in-memory fields are safe to read and write without a lock. Use an object, not module state, when two calls must never interleave on the same data.