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 ofcrypto,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 devruns the Worker locally with a Miniflare emulator that approximates the production runtime. Fast iteration loop.wrangler deployships it to production.wrangler tailstreams logs from the deployed Worker in real time.wrangler secret put NAMEsecurely sets a secret value the Worker reads viaenv.NAME. Secrets never appear inwrangler.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 subdomainyoursite.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.