---
title: "02 — Request Handling, Fetch, and the Workers Lifecycle"
uid: workers-request-handling
tags: ["lifecycle", "bindings", "workers", "cloudflare", "roadmap:cloudflare", "fetch-api", "request-response", "middleware"]
excerpt: "A Worker is fundamentally one fetch handler — Request in, Response out. Bindings, caching, middleware, service bindings: plumbing attached to that single exchange."
date: 2026-08-13T03:28:19+0000
source: https://www.aveshina.my.id/en/blog/workers-request-handling
---

"It runs your code on a request" described what a Worker does without explaining its shape. The model that straightened it out: **a Worker is fundamentally a single fetch handler that receives a Request and returns a Response, and everything else — bindings, caching, middleware, service bindings — is plumbing attached to that one exchange.** [1] Once I saw the request/response cycle as the spine, every other feature snapped into place around it.

The framing that landed is that a Worker is a function with a specific signature. The whole platform is organized around that signature, and the lifecycle is short: the request arrives, the handler runs, the response leaves, the isolate is done [2]. State that needs to survive past that one exchange lives in bindings, not in the Worker's own memory.

```figure
<svg viewBox="0 0 740 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The Worker request lifecycle. A request enters from the left, flows through optional middleware (auth, logging), reaches the fetch handler which consults bindings (KV, D1, R2, service bindings) and possibly fetches from origin, then a Response exits to the right. A small note says the isolate terminates after the response is sent.">
  <defs>
    <marker id="rarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- input -->
    <rect x="20" y="130" width="90" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="65" y="153" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Request</text>
    <text x="65" y="168" font-size="9" fill="#475569" text-anchor="middle">method, url, headers</text>

    <!-- middleware chain -->
    <rect x="135" y="110" width="70" height="40" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="170" y="134" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">auth</text>
    <rect x="135" y="160" width="70" height="40" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="170" y="184" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">logging</text>

    <!-- fetch handler -->
    <rect x="240" y="130" width="120" height="50" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="300" y="153" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">fetch handler</text>
    <text x="300" y="168" font-size="9" fill="#052e16" text-anchor="middle">(request, env, ctx)</text>

    <!-- bindings -->
    <rect x="395" y="60" width="60" height="32" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="425" y="80" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">KV</text>
    <rect x="395" y="100" width="60" height="32" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="425" y="120" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">D1</text>
    <rect x="395" y="140" width="60" height="32" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="425" y="160" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">R2</text>
    <rect x="395" y="180" width="60" height="32" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="425" y="200" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">service</text>
    <text x="425" y="48" font-size="10" font-style="italic" fill="#500724" text-anchor="middle">bindings (env)</text>

    <!-- origin fetch -->
    <rect x="395" y="232" width="120" height="40" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="455" y="256" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">fetch(origin) optional</text>

    <!-- response -->
    <rect x="555" y="130" width="90" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="600" y="153" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Response</text>
    <text x="600" y="168" font-size="9" fill="#475569" text-anchor="middle">status, body, headers</text>

    <!-- flow arrows -->
    <path d="M110,155 L133,150" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M205,130 L238,145" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M205,180 L238,165" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M360,150 L393,140" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M360,160 L393,170" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M360,170 L393,210" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M300,180 L300,230 L393,250" fill="none" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#rarrow)"/>
    <path d="M515,150 L553,150" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>

    <text x="370" y="300" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">isolate terminates once the Response is returned</text>
  </g>
</svg>
```

## The fetch handler: the one entry point

A Worker exports one thing that matters: a fetch handler. Everything else is in service of it. The signature is fixed:

```
export default {
  async fetch(request, env, ctx) {
    return new Response("Hello");
  },
};
```

Three parameters, each load-bearing [1]:

- **request** — a standard web Request object. Method, URL, headers, body — all here. The same Request type the browser's Fetch API uses.
- **env** — the bindings: KV namespaces, D1 databases, R2 buckets, secrets, environment variables, service bindings to other Workers. This is how the Worker touches anything outside itself.
- **ctx** — the execution context. Its killer feature is ctx.waitUntil(promise), which lets work continue briefly after the response is sent (for logging, analytics, cache writes).

The contract is: take a Request, return a Response. That's it. Everything in the Workers ecosystem is an elaboration of that one function.

## The Fetch API and runtime APIs

Inside the handler, the Fetch API is the primary tool for reaching outward — calling an origin server, a third-party API, another service [3]. It's the browser's fetch, adapted for the server side:

```
const upstream = await fetch("https://api.example.com/data", {
  headers: { Authorization: `Bearer ${env.API_KEY}` },
});
```

The runtime also exposes a set of Cloudflare-specific APIs on top of the web standards — caches for the Cache API, the KV binding methods, HTMLRewriter for streaming HTML modification. These aren't globals in the Node sense; they're either always-available (caches, crypto) or attached to a binding (KV, D1) [3]. The mental shift from Node is: nothing is imported from a package, everything is handed to you via env or available as a web standard.

## The lifecycle: short, stateless, disposable

The lifecycle is the part I had to internalize before statelessness stopped feeling like a limitation [2]:

1. A request hits the edge location nearest the user.
2. Cloudflare routes it to a Worker isolate (spawning one if none is warm).
3. The fetch handler runs, consulting bindings and upstream APIs as needed.
4. The Response is returned to the user.
5. The isolate is done — terminated, or kept warm briefly for the next request on the same edge.

There is **no persistent state between requests** unless I explicitly put it in a binding. Global variables set in one invocation are visible to subsequent invocations on the same warm isolate, but that's an optimization, not a contract — I can never rely on a global surviving. State I need to keep goes in KV (eventually consistent, global), D1 (SQL), or Durable Objects (strongly consistent, single-actor) [2].

## Bindings: the only way out

Bindings are the configuration that connects a Worker to anything external, and they're declared in wrangler.toml rather than in code [4]. The categories:

- **KV namespaces** — read-heavy key-value storage.
- **Durable Objects** — strongly consistent stateful objects.
- **R2 buckets** — object storage.
- **D1 databases** — SQL.
- **Queues** — message queues (producer and consumer).
- **Service bindings** — direct calls to other Workers, no network hop.
- **Secrets** — encrypted environment variables (API keys).
- **Environment variables** — non-secret config.

The reason bindings matter as a concept is that they replace what I'd otherwise do with connection strings and API keys scattered through code. A KV namespace isn't a URL my Worker connects to — it's an object my Worker is _given_, in env. The binding is the relationship; the resource is behind it [4].

## Service bindings: Worker-to-Worker, no network

Service bindings deserve a callout because they're the cleanest composition primitive [5]. A service binding lets one Worker call another as if it were a function — the caller invokes env.OTHER_WORKER.fetch(request), and the call stays inside Cloudflare's network, never touching the public internet. No API key, no DNS, no TLS handshake overhead, no egress.

This is what makes microservice-style architectures viable on Workers. Splitting a monolithic Worker into several smaller ones — one per bounded domain — doesn't cost a network round trip per call, the way it would if they communicated over HTTP. The binding is a direct in-network channel [5].

## Caching strategies

Workers give fine-grained cache control via the Cache API — I can read from and write to Cloudflare's cache programmatically, not just via HTTP headers [6]. The three patterns worth memorizing:

- **Cache-first** — check cache; if hit, return it; if miss, fetch from origin, cache, return. Best for static content.
- **Network-first** — always fetch from origin, then refresh the cache. Best for content that must be fresh.
- **Stale-while-revalidate** — return cached immediately if present, then fetch the fresh version in the background for next time. Best for content where slightly-stale is acceptable but you want eventual freshness.

The choice is a freshness-versus-latency trade. Stale-while-revalidate is the default I reach for — it gives instant responses from cache while quietly fixing the staleness in the background.

## Middleware patterns

Middleware is just composition — chaining handlers so each does one job (auth, logging, header injection) before passing the request to the next [7]. Workers don't ship a middleware system in the box, but frameworks like Hono and patterns like the one in the reference make it a solved problem. The shape is always the same: a function takes the request and a next function, does its thing, and either short-circuits with a response or calls next() to continue the chain.

This is where code organization pays off. A Worker that grew into a tangle of inline auth and logging logic is a candidate for middleware extraction — the request flows through a pipeline, each stage has one responsibility [7].

## Logging and monitoring

The bare essentials: console.log in development, wrangler tail in production for real-time logs streaming from the edge [8]. For anything beyond poking around, the metrics that matter are request count, CPU time per request, error rate, and cache hit ratio — all surfaced in the dashboard. The habit I built: log structured JSON, not strings, so that when I ship logs to an external service they're queryable rather than grep-fodder.

## How I use this

The request/response cycle is the spine of every Worker I write, so I keep the handler thin: middleware for cross-cutting concerns, the handler for the actual logic, bindings for any state, and ctx.waitUntil for anything that doesn't need to block the response. When a Worker starts feeling complex, the question I ask is whether I'm trying to put too much in one handler — at which point service bindings and a split into multiple Workers usually clarify things.

## References

[1] Cloudflare, "Request and Response," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/request](https://developers.cloudflare.com/workers/runtime-apis/request)

[2] Cloudflare, "How Workers works," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/reference/how-workers-works/](https://developers.cloudflare.com/workers/reference/how-workers-works/)

[3] Cloudflare, "Fetch API in Workers," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch)

[4] Cloudflare, "Bindings (env)," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/bindings/](https://developers.cloudflare.com/workers/runtime-apis/bindings/)

[5] Cloudflare, "Service Bindings," Cloudflare Workers Platform, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/platform/service-bindings/](https://developers.cloudflare.com/workers/platform/service-bindings/)

[6] Cloudflare, "How the cache works · Cloudflare Workers," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/reference/how-the-cache-works/](https://developers.cloudflare.com/workers/reference/how-the-cache-works/)

[7] Cloudflare, "Middleware · Cloudflare Pages Functions," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/pages/functions/middleware/](https://developers.cloudflare.com/pages/functions/middleware/)

[8] Cloudflare, "Debugging and logging · Cloudflare Pages," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/pages/functions/debugging-and-logging/](https://developers.cloudflare.com/pages/functions/debugging-and-logging/)

```quiz
Q: What is the required entry point for a Cloudflare Worker?
- A `main()` function, like in C
- An exported `fetch(request, env, ctx)` handler that returns a Response
- An Express-style app with `app.get()` routes
correct: 1
explain: A Worker exports an object with a `fetch` handler. The whole platform is organized around that one signature: take a Request, return a Response.

Q: What is the role of the `env` parameter in a fetch handler?
- It holds environment variables only
- It carries all bindings — KV, D1, R2, secrets, service bindings, and env vars
- It's the Node.js `process.env` object
correct: 1
explain: `env` is how the Worker touches anything external. Each binding declared in wrangler.toml appears as a property on `env`, replacing the connection strings and API keys you'd otherwise hardcode.

Q: After a Worker returns its Response, what happens to the isolate?
- It stays alive forever, holding all global state
- It terminates (or is kept warm briefly), with no guaranteed persistent state
- It spawns a child isolate for the next request
correct: 1
explain: The lifecycle is short and stateless. Globals may survive briefly on a warm isolate as an optimization, but you can never rely on it — durable state goes in bindings.

Q: What does `ctx.waitUntil(promise)` let you do?
- Pause the response until the promise resolves
- Keep the isolate alive briefly after the response for background work like logging
- Block the request from reaching the handler
correct: 1
explain: waitUntil extends the isolate's lifetime past the response so background tasks (analytics, cache writes) can finish without delaying the user.

Q: What's the key advantage of a service binding over calling another Worker via HTTP?
- Service bindings support authentication automatically
- Service bindings stay inside Cloudflare's network — no public internet, no TLS handshake, no egress
- Service bindings are cheaper because they skip the cache
correct: 1
explain: A service binding is a direct in-network channel between Workers. It removes the network round trip and auth overhead, making Worker-to-Worker composition cheap.
```
