Cloudflare Workers skill for Claude Code
Right storage per binding, typed env, one-command rollback. Claude configures Workers with explicit compatibility dates, generated binding types and per-environment bindings, then picks KV, D1, R2, Durable Objects or Queues from the access pattern rather than habit. Code stays inside CPU and memory limits, tests run in the real Workers runtime via Vitest, and releases go out as versions with traffic splitting and a known rollback.
- name
cloudflare-workers - size203 lines · ≈2,350 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and deploys Cloudflare Workers with Wrangler - wrangler.jsonc configuration, compatibility dates and nodejs_compat, typed bindings generated by wrangler types, KV, D1, R2, Durable Objects, Queues and Hyperdrive, secrets and environments, local development with wrangler dev, Vitest in the Workers runtime, platform limits, gradual deployments and rollback, and observability. Use when creating a Worker, adding a binding, choosing between KV, D1, R2 and Durable Objects, hitting CPU or subrequest limits, or debugging behavior that differs between local and deployed Workers. Try asking
- Set up a Worker with D1, R2 and a queue consumer using wrangler.jsonc
- Should these counters live in KV, D1 or a Durable Object?
- Our Worker works in wrangler dev but fails after deploy. Investigate
Use it when
- Creating or configuring a Cloudflare Worker
- Adding KV, D1, R2, Durable Objects, Queues or Hyperdrive bindings
- Hitting CPU, memory or subrequest limits
- Local behavior differs from the deployed Worker
Skip it when
- You need routing, validators and RPC types in app code — use hono-edge
- The question is R2 presigned uploads and bucket policy — use s3-object-storage
How to check the result
wrangler types and tsc pass, Vitest runs green in the Workers pool, and after versions deploy the tail shows no exceptions with CPU time well under the limit.
The common mistake it prevents
Using KV for counters, locks or read-after-write data, where eventual consistency returns stale values; that state belongs in a Durable Object.
A well-built Worker declares every binding in Wrangler config with generated types, uses each storage product for what it is good at, stays inside CPU and subrequest limits by design, keeps secrets out of config, runs tests in the real Workers runtime, and deploys through versions so a bad release rolls back in one command.
Workflow
- [ ] 1. Read wrangler.jsonc/toml: name, main, compatibility_date/flags, bindings, envs
- [ ] 2. Pick storage per access pattern (table below)
- [ ] 3. Declare bindings; run wrangler types; code against typed env
- [ ] 4. Local dev with wrangler dev (local bindings); seed D1/KV as needed
- [ ] 5. Tests with @cloudflare/vitest-pool-workers
- [ ] 6. Secrets per environment; deploy a version; promote gradually
- [ ] 7. Observe with logs/tail; check CPU time and errors
Configuration
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "orders-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-01",
"compatibility_flags": ["nodejs_compat"],
"observability": { "enabled": true },
"kv_namespaces": [{ "binding": "CACHE", "id": "<namespace-id>" }],
"d1_databases": [{ "binding": "DB", "database_name": "orders", "database_id": "<uuid>", "migrations_dir": "migrations" }],
"r2_buckets": [{ "binding": "FILES", "bucket_name": "orders-files" }],
"durable_objects": { "bindings": [{ "name": "ROOMS", "class_name": "Room" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Room"] }],
"queues": {
"producers": [{ "binding": "JOBS", "queue": "orders-jobs" }],
"consumers": [{ "queue": "orders-jobs", "max_batch_size": 10, "max_retries": 5, "dead_letter_queue": "orders-jobs-dlq" }]
},
"vars": { "APP_ENV": "production" },
"env": {
"staging": { "vars": { "APP_ENV": "staging" }, "d1_databases": [{ "binding": "DB", "database_name": "orders-staging", "database_id": "<uuid>" }] }
}
}
compatibility_date: set to the date you start a project and bump deliberately; it changes runtime behavior.nodejs_compatenables Node built-ins (node:crypto,node:buffer,AsyncLocalStorage, many npm packages). Without it, Node-dependent libraries fail at runtime.- Bindings are not inherited by named environments: repeat them under
env.<name>. - Run
npx wrangler typesafter changing bindings; it generates theEnvinterface (worker-configuration.d.ts) soenv.DBis typed. Commit or regenerate in CI.
Storage choice
| Need | Use | Key properties |
|---|---|---|
| Read-heavy config, flags, cached API responses, sessions | KV | Global, eventually consistent (changes can take up to about 60 s to propagate), low write rate per key (about 1/s) |
| Relational data, queries, transactions per request | D1 (SQLite) | Single primary per database with read replication option; batch statements; size-limited per DB, so shard per tenant at scale |
| Files, blobs, user uploads, large objects | R2 | S3-compatible, no egress fees, strong consistency |
| Coordination, counters, rate limits, websockets, per-entity state | Durable Objects | One instance per ID, single-threaded, strongly consistent SQLite storage, alarms |
| Async work, retries, batching | Queues | At-least-once, batching, DLQ |
| Existing Postgres/MySQL | Hyperdrive | Pools and caches connections from Workers to your database |
| Multi-step durable processes | Workflows | Steps with retries and sleeps, state persisted |
Worker code
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
if (url.pathname.startsWith("/orders/") && req.method === "GET") {
const id = url.pathname.split("/")[2];
const cached = await env.CACHE.get(`order:${id}`, "json");
if (cached) return Response.json(cached);
const row = await env.DB.prepare("SELECT id, total, status FROM orders WHERE id = ?").bind(id).first();
if (!row) return new Response("not found", { status: 404 });
ctx.waitUntil(env.CACHE.put(`order:${id}`, JSON.stringify(row), { expirationTtl: 60 }));
return Response.json(row);
}
return new Response("not found", { status: 404 });
},
async queue(batch: MessageBatch<{ orderId: string }>, env: Env): Promise<void> {
for (const msg of batch.messages) {
try { await processOrder(env, msg.body.orderId); msg.ack(); }
catch { msg.retry({ delaySeconds: 30 }); }
}
},
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(sweepExpired(env));
},
} satisfies ExportedHandler<Env>;
- Always
.bind()parameters in D1; never interpolate. Useenv.DB.batch([...])for multiple statements in one round trip (executed as a transaction). ctx.waitUntilextends the request lifetime for background work after the response; it is not durable. Use Queues for anything that must happen.- No global mutable state across requests for correctness: isolates are reused unpredictably and not shared across locations.
Durable Object with SQLite storage:
import { DurableObject } from "cloudflare:workers";
export class Room extends DurableObject<Env> {
async increment(): Promise<number> {
this.ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS c (k TEXT PRIMARY KEY, v INTEGER)");
this.ctx.storage.sql.exec("INSERT INTO c VALUES ('n', 1) ON CONFLICT(k) DO UPDATE SET v = v + 1");
return this.ctx.storage.sql.exec("SELECT v FROM c WHERE k = 'n'").one().v as number;
}
}
// caller: const stub = env.ROOMS.get(env.ROOMS.idFromName(roomId)); await stub.increment();
Use the WebSocket Hibernation API (this.ctx.acceptWebSocket(ws)) for many idle sockets.
Limits to design around
Check the current limits page for exact numbers; these shape architecture:
- CPU time per invocation: about 10 ms on Free; paid plans default to 30 s and can be
raised (
"limits": { "cpu_ms": 300000 }). Wall time waiting on I/O does not count. - Memory: 128 MB per isolate. Stream large bodies instead of buffering.
- Subrequests (fetch, binding calls) per invocation are capped and differ by plan.
- Script size limits after compression differ by plan; heavy dependencies hurt startup.
- D1 and KV have per-value and per-database size limits.
CPU-heavy work (image processing, large JSON transforms) belongs in Queues consumers, Containers, or another service.
Local development and tests
npx wrangler dev # local runtime (workerd) with local bindings
npx wrangler dev --remote # run on Cloudflare's edge with real resources
npx wrangler d1 migrations create orders add_status
npx wrangler d1 migrations apply orders --local
npx wrangler d1 execute orders --local --command "SELECT count(*) FROM orders"
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.jsonc" } } } } });
// test/index.test.ts
import { SELF, env } from "cloudflare:test";
it("returns 404 for unknown order", async () => {
const res = await SELF.fetch("https://example.com/orders/nope");
expect(res.status).toBe(404);
});
Local secrets go in .dev.vars (gitignored), mirroring production secret names.
Secrets and deploys
npx wrangler secret put STRIPE_KEY # production
npx wrangler secret put STRIPE_KEY --env staging
npx wrangler d1 migrations apply orders --remote # before deploying code that needs it
npx wrangler deploy --env staging
npx wrangler versions upload # upload without serving traffic
npx wrangler versions deploy # interactive: split traffic between versions
npx wrangler rollback # previous version
npx wrangler tail orders-api --format pretty # live logs
Production deploys, remote D1 migrations, and deleting KV namespaces, D1 databases or R2
buckets require explicit user confirmation. Remote D1 migrations are not automatically
reversible; back up first with wrangler d1 export orders --remote --output backup.sql
or rely on D1 Time Travel restore.
Gotchas
wrangler devlocal mode uses local copies of D1/KV/R2; data differs from production.- KV
getright afterputfrom another location can return stale data; never use KV for counters or locks (use Durable Objects). - Responses with
Set-Cookieor authorization are not cached by default; use the Cache API deliberately with explicit keys. fetchto your own zone from a Worker on the same route can loop; use service bindings for Worker-to-Worker calls.- Date and timers advance only during I/O inside a request (Spectre mitigation), so
Date.now()differences measure I/O, not CPU.
Verification
npx wrangler typesis clean,tsc --noEmitpasses, and vitest runs green in the Workers pool.wrangler devserves expected responses with local bindings; remote migrations applied before code that needs them.- After
versions deploy,wrangler tailshows no exceptions and the dashboard shows CPU time well under the limit; rollback command known and tested on staging.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.