Cloudflare Workers
Setup
npm install -g wrangler && wrangler login
wrangler init my-worker
wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
ENVIRONMENT = "production"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "def456"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
Worker Basics
interface Env {
MY_KV: KVNamespace; DB: D1Database; BUCKET: R2Bucket;
ENVIRONMENT: string; API_KEY: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/health") return Response.json({ status: "ok" });
if (request.method === "POST" && url.pathname === "/api/data") {
const body = await request.json();
return new Response("Created", { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
};
Wrangler CLI
wrangler dev # local dev server on localhost:8787
wrangler dev --remote # dev against real Cloudflare infrastructure
wrangler deploy # deploy to production
wrangler tail # stream live logs from deployed worker
wrangler secret put API_KEY # set an encrypted secret
wrangler secret list # list configured secrets
wrangler delete # remove the deployed worker
Routing
Manual routing with a map, or use hono/itty-router for path params:
// Manual
const routes: Record<string, () => Promise<Response>> = {
"/api/users": () => handleUsers(request),
"/api/posts": () => handlePosts(request),
};
const handler = routes[new URL(request.url).pathname];
if (handler) return handler();
// Hono (recommended for complex routing)
import { Hono } from "hono";
const app = new Hono<{ Bindings: Env }>();
app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }));
export default app;
KV Store
Global, low-latency key-value store. Eventually consistent. Best for read-heavy data.
wrangler kv namespace create MY_KV
wrangler kv namespace create MY_KV --preview
await env.MY_KV.put("user:123", JSON.stringify({ name: "Alice" }), {
expirationTtl: 3600, metadata: { created: Date.now() },
});
const value = await env.MY_KV.get("user:123", "json");
const list = await env.MY_KV.list({ prefix: "user:", limit: 100 });
await env.MY_KV.delete("user:123");
D1 Database
SQLite at the edge with queries, transactions, and migrations.
wrangler d1 create my-db
wrangler d1 execute my-db --command "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
wrangler d1 migrations create my-db init
wrangler d1 migrations apply my-db
const { results } = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(userId).all();
await env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run();
await env.DB.batch([
env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Bob"),
env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Carol"),
]);
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(1).first();
R2 Storage
S3-compatible object storage with no egress fees.
wrangler r2 bucket create my-bucket
// Upload
await env.BUCKET.put("images/photo.jpg", imageData, {
httpMetadata: { contentType: "image/jpeg" },
});
// Download
const object = await env.BUCKET.get("images/photo.jpg");
if (object) {
return new Response(object.body, {
headers: { "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream" },
});
}
// List, delete
const listed = await env.BUCKET.list({ prefix: "images/", limit: 50 });
await env.BUCKET.delete("images/photo.jpg");
For large files use createMultipartUpload(), uploadPart(), complete().
Durable Objects
Strongly consistent, stateful compute. Each object has a unique ID and private storage. Use for rate limiters, WebSocket coordination, collaborative editing, session state.
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
export class Counter implements DurableObject {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const current = (await this.state.storage.get<number>("count")) ?? 0;
await this.state.storage.put("count", current + 1);
return Response.json({ count: current + 1 });
}
}
// Calling from a worker:
const id = env.COUNTER.idFromName("my-counter");
const response = await env.COUNTER.get(id).fetch(request);
Workers AI
Run ML models at the edge. Add [ai] with binding = "AI" to wrangler.toml.
// Text generation
const resp = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [{ role: "user", content: "Summarize this article." }],
});
// Embeddings
const emb = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: ["document to embed"] });
// Image classification
const cls = await env.AI.run("@cf/microsoft/resnet-50", { image: await request.arrayBuffer() });
Environment Variables and Secrets
Non-sensitive values go in wrangler.toml under [vars]. Set secrets with wrangler secret put API_KEY. Both accessed through env.API_KEY, env.ENVIRONMENT, etc.
Multiple environments:
[env.staging]
name = "my-worker-staging"
vars = { ENVIRONMENT = "staging" }
[env.production]
name = "my-worker-production"
vars = { ENVIRONMENT = "production" }
Deploy with wrangler deploy --env staging or --env production.
Cron Triggers
[triggers]
crons = ["0 */6 * * *", "30 8 * * 1"]
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
ctx.waitUntil(doCleanup(env));
},
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response("OK");
},
};
Test locally: curl "http://localhost:8787/__scheduled?cron=0+*/6+*+*+*"
Middleware Patterns
CORS
function corsHeaders(origin: string): HeadersInit {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
}
Auth
async function requireAuth(request: Request, env: Env): Promise<Response | null> {
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token || token !== env.API_KEY) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return null; // proceed
}
Rate Limiting
Use a Durable Object to track request timestamps per key. Store timestamps in storage, filter to the current window, reject if over limit, append and persist otherwise.
Local Development
wrangler dev # local server with miniflare runtime
wrangler dev --persist-to=./data # persist KV/D1/R2 data locally
wrangler dev --port 3000 # custom port
wrangler dev --remote # proxy to Cloudflare (real bindings)
Miniflare simulates KV, D1, R2, Durable Objects, and caches locally.
Deployment
wrangler deploy # deploy to production
wrangler deploy --env staging # named environment
wrangler deploy --dry-run # validate without deploying
wrangler versions list # list deployed versions
wrangler rollback # rollback to previous version
Custom domains:
routes = [{ pattern = "api.example.com/*", zone_name = "example.com" }]
Common Patterns
API Proxy
async function proxyRequest(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
url.hostname = "api.upstream.com";
return fetch(new Request(url.toString(), {
method: request.method,
headers: { ...Object.fromEntries(request.headers), "X-API-Key": env.UPSTREAM_KEY },
body: request.body,
}));
}
Edge Cache
async function cachedFetch(request: Request, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
let response = await cache.match(request);
if (response) return response;
response = await fetch("https://api.origin.com" + new URL(request.url).pathname);
const cached = new Response(response.body, response);
cached.headers.set("Cache-Control", "s-maxage=300");
ctx.waitUntil(cache.put(request, cached.clone()));
return cached;
}
Webhook Handler
async function handleWebhook(request: Request, env: Env): Promise<Response> {
const signature = request.headers.get("X-Signature-256") ?? "";
const body = await request.text();
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(env.WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" }, false, ["verify"]
);
const valid = await crypto.subtle.verify(
"HMAC", key, hexToBytes(signature.replace("sha256=", "")),
new TextEncoder().encode(body)
);
if (!valid) return new Response("Invalid signature", { status: 401 });
return new Response("OK", { status: 200 });
}
URL Shortener
app.post("/shorten", async (c) => {
const { url } = await c.req.json();
const id = crypto.randomUUID().slice(0, 8);
await c.env.MY_KV.put(`url:${id}`, url, { expirationTtl: 86400 * 30 });
return c.json({ short: `${new URL(c.req.url).origin}/${id}` });
});
app.get("/:id", async (c) => {
const target = await c.env.MY_KV.get(`url:${c.req.param("id")}`);
if (!target) return c.text("Not found", 404);
return c.redirect(target, 302);
});
Limits
| Resource | Free | Paid |
|---|---|---|
| CPU time/request | 10 ms | 30 s (Unbound) / 50 ms |
| Memory | 128 MB | 128 MB |
| Worker size | 1 MB | 10 MB |
| Subrequests (fetch) | 50 | 1000 |
| KV reads/day | 100K | 10M+ |
| KV writes/day | 1K | 1M+ |
| D1 rows read/day | 5M | 50B |
| D1 rows written/day | 100K | 50M |
| R2 Class A ops/month | 1M | $4.50/M |
| R2 storage | 10 GB | $0.015/GB-mo |
| Cron triggers | 3 | 3+ |
| Request body size | 100 MB | 100 MB |
Key constraints: no raw TCP/UDP sockets (use WebSockets or Tunnels). crypto.subtle available. Node.js built-ins partially supported via nodejs_compat flag. Globals persist within an isolate but not across cold starts. ctx.waitUntil() extends execution after response for background work.