← route

LSN 06/10 WORKERS 14 MIN TIER FREE

Workers: code at the edge

JavaScript that runs at the proxy on every request. The programming model, the toolchain, and what they replace.

Before this: 05 Pages: hosting your first site

Pages serves files. Workers run code. They are the same runtime, deployed differently. If Pages alone cannot do what you need, Workers are the next thing to reach for.

What a Worker looks like

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/ping") {
      return new Response("pong", { status: 200 });
    }
    return new Response("not found", { status: 404 });
  },
};

That is a complete Worker. Deploy it and every request to the hostname you bind it to will hit this function.

The signature is the Fetch API standard, the same API browsers use for fetch() and Service Workers use for addEventListener("fetch"). If you have written browser JavaScript, you mostly know this. Request and Response objects have body, headers, url, method, etc.

env holds bindings (KV namespaces, R2 buckets, D1 databases, secrets) declared in your config. ctx.waitUntil(promise) lets you continue work after the response has been sent.

The runtime

Workers run on V8 isolates, not Node and not containers. Practical consequences:

  • Cold starts are sub-millisecond. A fresh isolate is created in microseconds. There is no warm-up problem, no provisioning, no scale-to-zero penalty. This is the central feature.
  • No filesystem, no fs.readFile. You bundle everything into the Worker at build time, or fetch from R2 / KV / external service at request time.
  • No native modules. A Node package that depends on native bindings (sharp, sqlite3, bcrypt) will not work.
  • A subset of Node APIs is available when you opt into Node compatibility (compatibility_flags = ["nodejs_compat"]). Buffer, process, parts of crypto, util, stream. Coverage has been growing.
  • CPU time per request is capped. 10ms on free, 30 seconds on paid. Wall-clock time can be longer (waiting for fetch, KV, etc). The 10ms cap on free is a real constraint; check it before assuming your Worker fits.

The constraints are tight on purpose. They are why the cold start is zero.

The toolchain: wrangler

wrangler is the CLI for Workers. Install once:

npm install -g wrangler
# or use the project-local version: npm install --save-dev wrangler

A minimal project layout:

my-worker/
├── wrangler.toml
└── src/
    └── index.ts

wrangler.toml declares the deployment metadata:

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-05-01"

# routes — what hostnames/paths this Worker handles
routes = [
  { pattern = "api.yoursite.com/*", zone_name = "yoursite.com" }
]

# bindings
kv_namespaces = [
  { binding = "CACHE", id = "abc123..." }
]

[vars]
LOG_LEVEL = "info"

Useful commands:

  • wrangler dev runs the Worker locally with a Miniflare emulator that approximates the production runtime. Fast iteration loop.
  • wrangler deploy ships it to production.
  • wrangler tail streams logs from the deployed Worker in real time.
  • wrangler secret put NAME securely sets a secret value the Worker reads via env.NAME. Secrets never appear in wrangler.toml.

Routes

A Worker is bound to one or more routes. The route pattern can be a hostname, a path, or both:

  • api.yoursite.com/* — every request to that subdomain
  • yoursite.com/api/* — only that path on the apex
  • *.yoursite.com/* — every subdomain (wildcard)

For the route to match, the hostname must have an orange-cloud DNS record on Cloudflare. The record’s IP value does not matter for a Worker route, since the Worker handles the request before it ever reaches an origin. A common idiom is to point the DNS at a placeholder IP (like 192.0.2.1) and let the Worker take it from there.

Bindings

Bindings are how a Worker accesses other Cloudflare resources. They are declared in wrangler.toml and surface as fields on env at runtime:

await env.CACHE.put("key", "value");          // KV
await env.BUCKET.put("file.jpg", body);       // R2
const rows = await env.DB.prepare(           // D1
  "SELECT * FROM users WHERE id = ?"
).bind(userId).all();
const r = await env.AI.run("@cf/meta/llama-2-7b-chat-int8", { ... });

The next lesson covers these storage primitives in detail.

Workers vs Pages Functions

The runtime is identical. The difference is the deployment unit:

  • Standalone Worker. Its own project, its own wrangler.toml, its own deploys, its own routes. Use when the code is independent of any particular site.
  • Pages Function. A file inside a Pages project, deployed in lockstep with the static site. Use when the code is tightly tied to that site’s frontend.

For a small project, Pages Functions are easier. For a Worker that serves many sites, or for any production API of meaningful size, standalone is cleaner.

Pricing

  • Free plan: 100,000 requests per day, 10ms CPU per request, shared bandwidth.
  • Workers Paid ($5/mo base): 10 million requests per month included, 30s CPU per request, then $0.30 per million requests.

The free quota is enough for most personal projects. Bundled paid quotas are enough for low-traffic production. Egress is free as always with Cloudflare.

When to reach for a Worker

The high-leverage cases:

  • API endpoints for a static site (form submissions, lightweight queries, webhook receivers).
  • Auth middleware in front of a backend (JWT verification, rate limiting).
  • Edge transforms (rewrite responses, A/B variants, geo-routing).
  • Aggregating multiple APIs into one frontend-friendly response.
  • AI inference at the edge via Workers AI.
  • Cron jobs via Scheduled Workers.

The cases where Workers are the wrong tool: anything CPU-heavy beyond 30 seconds, anything needing persistent local filesystem state, anything depending on native Node modules. Use a real server for those.

Storage primitives come next.