After this lesson you'll be able to write a minimal TypeScript Worker, run it locally with wrangler dev, and ship it to Cloudflare's network with wrangler deploy.
A Worker is not a container and not a VM. It's a piece of JavaScript (or WASM) running inside a V8 isolate — the same isolation mechanism Chrome uses to separate browser tabs. Cloudflare's runtime (workerd) keeps hundreds of isolates for different customers resident in one process, switching between them in sub-millisecond time. There's no OS to boot, no container filesystem to mount, no kernel per tenant — just a JS context with memory and CPU limits enforced by V8 itself.
This is why Workers start cold in about 5ms instead of the hundreds of milliseconds (or seconds) typical of container-based serverless cold starts. It's also why the billing and limits model looks different from Lambda-style compute: you're metered on CPU time actually spent executing your code, not on wall-clock time the process was alive.
When a request hits a Worker route, it lands at the nearest Cloudflare data center to the requester — not a single "us-east-1" region. From there:
fetch handler runs, receiving the request, environment bindings, and an execution context.Response (or a Promise that resolves to one) — synchronously in your code's flow, or after await-ing other work (a KV read, an upstream fetch(), a D1 query).ctx.waitUntil() keeps running after the response is sent, for up to 30 seconds, without making the client wait.Nothing here is region-pinned by you. You write one handler; Cloudflare runs a copy of it in every data center that receives traffic for your route.
Wrangler is Cloudflare's CLI for developing and deploying Workers. The fastest path is the official scaffolding tool, which installs Wrangler for you:
npm create cloudflare@latest -- my-first-worker
cd my-first-worker
Pick the "Hello World" Worker template and TypeScript when prompted. If you'd rather add Wrangler to an existing project directly:
npm install --save-dev wrangler
Wrangler requires Node.js 16.17.0 or later.
Every Worker's entrypoint exports a default object with a fetch handler. It always receives three arguments: the incoming Request, your env bindings, and an ExecutionContext:
// src/index.ts
export interface Env {
// Bindings (KV, D1, secrets, etc.) go here — none needed yet.
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response("Hello World!");
},
} satisfies ExportedHandler<Env>;
The ExportedHandler and ExecutionContext types come from @cloudflare/workers-types, which the scaffolding tool installs and wires into tsconfig.json for you.
Wrangler needs three fields, at minimum, to know what to deploy and how to run it:
# wrangler.toml
name = "my-first-worker"
main = "src/index.ts"
compatibility_date = "2026-07-03"
name — your Worker's identifier; also becomes part of its *.workers.dev URL.main — the entrypoint file Wrangler bundles and deploys.compatibility_date — pins which version of the Workers runtime behavior you get, so a runtime upgrade months from now can't silently change your Worker's behavior. Set it to today's date on a new project and bump it deliberately later.wrangler.jsonc (JSON with comments) instead of wrangler.toml — same fields, different syntax. Both are fully supported; this lesson uses TOML because it's still the more common format in existing docs and tutorials, but don't be surprised if create-cloudflare hands you a .jsonc file instead.
A small Worker that serves a JSON API with two routes — no framework, no build step beyond what Wrangler already does:
// src/index.ts
export interface Env {}
interface Task {
id: number;
title: string;
done: boolean;
}
const TASKS: Task[] = [
{ id: 1, title: "Write lesson", done: false },
{ id: 2, title: "Deploy Worker", done: false },
];
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/tasks" && request.method === "GET") {
return Response.json(TASKS);
}
if (url.pathname.match(/^\/api\/tasks\/\d+$/) && request.method === "GET") {
const id = Number(url.pathname.split("/").pop());
const task = TASKS.find((t) => t.id === id);
if (!task) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json(task);
}
return Response.json({ error: "Not found" }, { status: 404 });
},
} satisfies ExportedHandler<Env>;
Run it locally:
npx wrangler dev
# → Ready on http://localhost:8787
curl http://localhost:8787/api/tasks
wrangler dev runs your Worker in a local instance of the actual Workers runtime (not a Node.js shim), so behavior matches production closely — including things like the Request/Response API surface and the absence of Node built-ins unless you've enabled compatibility flags for them.
Ship it:
npx wrangler deploy
# → Uploaded my-first-worker
# → Published my-first-worker
# https://my-first-worker.<your-subdomain>.workers.dev
That URL is live on Cloudflare's network globally within seconds — no region selection, no separate CDN configuration.
Workers pricing has two plans, billed primarily on requests and CPU time, not wall-clock duration:
| Plan | Requests | CPU time | Cost |
|---|---|---|---|
| Free | 100,000 / day | 10ms per invocation | $0 |
| Paid | 10 million / month included, then $0.30 per additional million | 30 million CPU-ms / month included, then $0.02 per additional million CPU-ms | $5/month minimum |
Requests to static assets served by a Worker are free and unlimited, and subrequests your Worker makes (to KV, D1, another API, etc.) don't incur their own per-request Workers charge. Figures above reflect Cloudflare's published Workers pricing as of this writing — check the primary source below before relying on exact numbers, since pricing pages are the part of any cloud platform most likely to change without notice.
fetch() call, a KV read, or a database query does not count against it, because the isolate isn't burning CPU while it waits on I/O. This means a Worker that makes three sequential upstream API calls taking 200ms each can comfortably run on the Free plan (near-zero CPU time spent), while a Worker doing heavy in-process JSON parsing, encryption, or image manipulation on a small payload can blow through 10ms of CPU time almost instantly. If you see "Exceeded CPU Time Limit" errors, look at what your code is computing, not how long the request took end to end — and note the Paid plan raises this to 5 minutes of CPU time per request (30 seconds by default, configurable via limits.cpu_ms in your Wrangler config).
Cloudflare Workers Pricing is the canonical, most-frequently-updated source for exact request and CPU-time rates — re-check it before making cost commitments, since the numbers above are a snapshot as of 2026-07-03.
fetch(), each taking about 300ms to respond, then returns a small JSON response. Will this likely exceed the Free plan's CPU time limit?name (the Worker's identifier), main (path to the entrypoint file), and compatibility_date (a date pinning which version of Workers runtime behavior your code gets). compatibility_date exists so that Cloudflare can ship runtime changes and new defaults without silently breaking Workers that were written against older behavior — you opt into newer behavior by bumping the date deliberately.