Cloudflare Workers: Edge Functions for JavaScript Developers
Cloudflare Workers runs your JavaScript or TypeScript at the network edge — in 300+ data centers worldwide, milliseconds from any user. Unlike AWS Lambda (regional) or traditional servers (centralized), Workers execute at the location closest to the incoming request. This guide covers what Workers is, how to develop with it, and where it makes sense.
What Workers Is (and Isn't)
What it is:
- V8 isolate-based JavaScript/TypeScript runtime
- Executes at Cloudflare's edge, not a central region
- Sub-millisecond cold start (isolates, not containers)
- 0–10ms latency to the user (depending on their location)
- Part of Cloudflare's broader Workers platform (including KV, R2, D1, Durable Objects)
What it isn't:
- Node.js: Workers runs V8 with a subset of Web APIs, not full Node.js APIs
- Long-running: CPU time is limited (10ms on free tier, 30s on paid)
- General-purpose compute: Better for I/O-bound than CPU-intensive work
Getting Started
Install Wrangler (Workers CLI):
npm install -g wrangler
# or
bun add -g wrangler
Create a new project:
wrangler init my-worker
cd my-worker
This generates a wrangler.toml and a TypeScript worker:
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response("Hello World!");
},
} satisfies ExportedHandler<Env>;
Deploy:
wrangler deploy
Local development:
wrangler dev
wrangler dev runs a local server that closely emulates the Workers runtime, including KV, R2, and D1 bindings.
Request/Response Model
Workers use the standard Web Request and Response APIs:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Route handling
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello from the edge!" });
}
if (request.method === "POST" && url.pathname === "/api/data") {
const body = await request.json();
// process body...
return Response.json({ received: body }, { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
};
KV Storage
Workers KV is a global key-value store with eventual consistency. Data written in one region propagates worldwide within seconds.
Create a KV namespace:
wrangler kv:namespace create "MY_KV"
Add the binding in wrangler.toml:
[[kv_namespaces]]
name = "MY_KV"
id = "your-namespace-id"
Use it in your worker:
interface Env {
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Write
await env.MY_KV.put("user:1", JSON.stringify({ name: "Alice" }), {
expirationTtl: 86400 // expire after 1 day
});
// Read
const user = await env.MY_KV.get("user:1", "json");
// List keys
const list = await env.MY_KV.list({ prefix: "user:" });
return Response.json(user);
},
};
KV limits and use cases:
- Free tier: 100K reads/day, 1K writes/day
- Best for: caching, feature flags, user sessions, infrequently updated config
- Not ideal for: high-write-frequency data (use Durable Objects instead)
R2 Object Storage
R2 is S3-compatible object storage with no egress fees. Access from Workers:
interface Env {
MY_BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const key = new URL(request.url).pathname.slice(1);
if (request.method === "GET") {
const object = await env.MY_BUCKET.get(key);
if (!object) return new Response("Not found", { status: 404 });
return new Response(object.body, {
headers: { "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream" }
});
}
if (request.method === "PUT") {
await env.MY_BUCKET.put(key, request.body);
return new Response("Uploaded", { status: 201 });
}
return new Response("Method not allowed", { status: 405 });
},
};
D1: SQLite at the Edge
D1 is Cloudflare's serverless SQLite database, directly accessible from Workers:
wrangler d1 create my-db
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-db-id"
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { results } = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(1).all();
return Response.json(results);
},
};
D1 supports migrations, and you can run queries locally via wrangler d1 execute.
Durable Objects
Durable Objects provide stateful, singleton JavaScript objects with built-in storage. Each Durable Object has a unique ID and runs in exactly one location. Useful for: real-time collaboration, WebSocket hubs, rate limiting, transactional state.
export class Counter implements DurableObject {
private state: DurableObjectState;
private count = 0;
constructor(state: DurableObjectState) {
this.state = state;
// Restore persisted state
this.state.blockConcurrencyWhile(async () => {
this.count = (await this.state.storage.get<number>("count")) ?? 0;
});
}
async fetch(request: Request): Promise<Response> {
this.count++;
await this.state.storage.put("count", this.count);
return Response.json({ count: this.count });
}
}
Workers AI
Run ML inference at the edge using Cloudflare's GPU network:
interface Env {
AI: Ai;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [{ role: "user", content: "Summarize this text..." }]
});
return Response.json(response);
},
};
Available models include Llama 3, Mistral, Stable Diffusion, Whisper, and text embedding models.
Routing with Hono
Hono is a lightweight web framework built for edge runtimes. The standard choice for Workers with complex routing:
bun add hono
import { Hono } from "hono";
const app = new Hono<{ Bindings: Env }>();
app.get("/api/users/:id", async (c) => {
const id = c.req.param("id");
const user = await c.env.DB.prepare("SELECT * FROM users WHERE id = ?")
.bind(id).first();
return c.json(user);
});
app.post("/api/users", async (c) => {
const body = await c.req.json();
// create user...
return c.json({ created: true }, 201);
});
export default app;
When Workers Makes Sense
Good fits:
- API routes with low latency requirements: Auth, redirects, A/B testing
- Static site augmentation: Add dynamic behavior to static pages without a backend
- Geo-based routing: Route users to regional content based on their location
- Request transformation: Modify requests/responses mid-flight
- Webhook handlers: Receive and process webhooks without provisioning a server
- Rate limiting: Stateful rate limiting with Durable Objects
Poor fits:
- CPU-intensive computation: FFmpeg, image resizing (CPU limits are tight)
- Long-running processes: Max 30 seconds even on paid plans
- Complex Node.js apps: Many Node.js APIs aren't available in the Workers runtime
Pricing
| Free | Paid ($5/month base) | |
|---|---|---|
| Requests | 100K/day | 10M/month included |
| CPU time | 10ms | 30s |
| KV reads | 100K/day | 10M/month |
| KV writes | 1K/day | 1M/month |
For most hobby projects and low-traffic APIs, the free tier is sufficient. Worker pricing scales predictably — $0.50 per million requests beyond the included amount.
Local Development Workflow
# Start dev server with all bindings emulated locally
wrangler dev
# Run type checking
tsc --noEmit
# Deploy to preview (non-production)
wrangler deploy --env preview
# View logs in real-time
wrangler tail
Workers integrates with GitHub Actions for CI/CD:
- name: Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}