Cloudflare Workers Setup Reference
Cloudflare Workers setup reference
TL;DR
- Bottom line: Cloudflare Workers run JavaScript/TypeScript at the edge in 300+ data centers with sub-millisecond cold starts — configure everything in
wrangler.toml, deploy withnpx wrangler deploy. - Key tool/command:
npm create cloudflare@latest && npx wrangler deploy - Watch out for: CPU time limit (10 ms free / 30 s paid) is NOT wall-clock time — fetch() I/O wait doesn't count, but heavy JSON parsing will hit the limit.
- Works with: Wrangler v3+, Node.js 18+, TypeScript, bindings to KV, R2, D1, Queues, Durable Objects.
Constraints
- CPU time limit: 10 ms (free) / 30 s (paid) per invocation — I/O wait does not count.
- Worker script size: 10 MB compressed (paid), 1 MB (free) — includes all bundled dependencies.
- KV: 1 write/second per key, values up to 25 MB, keys up to 512 bytes.
- D1: max 10 GB per database, 6 simultaneous connections per invocation.
- Subrequests: max 1,000 per invocation (fetch, KV, R2, D1 operations combined).
- No raw TCP/UDP sockets — use connect() for TCP or Cloudflare Spectrum for proxying.
Quick Reference
| Setting | Value | Notes |
|---|---|---|
name | Worker name | Required — used as subdomain |
main | src/index.ts | Entry point (ES modules default) |
compatibility_date | 2026-02-28 | Required — controls runtime flags |
compatibility_flags | ["nodejs_compat_v2"] | Enables Node.js polyfills |
[vars] | key-value pairs | Non-secret env vars |
[[kv_namespaces]] | binding, id | KV namespace binding |
[[r2_buckets]] | binding, bucket_name | R2 object storage binding |
[[d1_databases]] | binding, database_id | D1 SQLite database binding |
[env.staging] | Environment overrides | Deploy with --env staging |
routes | [{pattern, zone_name}] | Custom domain routing |
workers_dev | true (default) | Enables *.workers.dev subdomain |
placement.mode | smart | Runs closer to backend |
[build] | command, cwd | Custom build command |
Decision Tree
START
├── Need key-value storage with global replication?
│ ├── YES → Use KV (eventually consistent, read-optimized)
│ └── NO ↓
├── Need relational queries (SQL)?
│ ├── YES → Use D1 (SQLite at the edge)
│ └── NO ↓
├── Need to store files/blobs (>25 MB)?
│ ├── YES → Use R2 (S3-compatible, no egress fees)
│ └── NO ↓
├── Need strong consistency / coordination?
│ ├── YES → Use Durable Objects (single-instance, stateful)
│ └── NO ↓
├── Need background processing?
│ ├── YES → Use Queues (at-least-once delivery)
│ └── NO ↓
└── DEFAULT → Plain Worker with fetch handler, no storage
Step-by-Step Guide
1. Create a new Workers project
Scaffold a project with Wrangler CLI. [src1]
npm create cloudflare@latest my-worker
cd my-worker
Verify: ls wrangler.toml src/index.ts → both files exist
2. Configure wrangler.toml
Set up bindings for KV, R2, D1. [src1] [src5]
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-02-28"
compatibility_flags = ["nodejs_compat_v2"]
[vars]
API_BASE = "https://api.example.com"
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456"
[[d1_databases]]
binding = "DB"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Verify: npx wrangler whoami → shows account
3. Write the Worker handler
Create a module Worker with typed bindings. [src4]
export interface Env {
CACHE: KVNamespace;
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}
return new Response('Not Found', { status: 404 });
}
} satisfies ExportedHandler<Env>;
Verify: npx wrangler dev → starts at localhost:8787
4. Deploy to production
Deploy the Worker to Cloudflare's edge. [src2]
npx wrangler deploy
Verify: curl https://my-worker.your-account.workers.dev/health → {"status":"ok"}
Code Examples
TypeScript: REST API with D1 and KV caching
// Input: HTTP request to /api/posts/:id
// Output: JSON response with post data, cached in KV
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
const match = url.pathname.match(/^\/api\/posts\/(\d+)$/);
if (!match) return new Response('Not Found', { status: 404 });
const cacheKey = `post:${match[1]}`;
const cached = await env.CACHE.get(cacheKey, 'json');
if (cached) return Response.json(cached);
const post = await env.DB.prepare(
'SELECT * FROM posts WHERE id = ?'
).bind(match[1]).first();
if (!post) return Response.json({ error: 'Not found' }, { status: 404 });
ctx.waitUntil(env.CACHE.put(cacheKey, JSON.stringify(post), { expirationTtl: 300 }));
return Response.json(post);
}
} satisfies ExportedHandler<Env>;
TypeScript: R2 file upload
// Input: PUT /upload/:filename with binary body
// Output: Stores file in R2, returns metadata
export default {
async fetch(request: Request, env: Env) {
if (request.method === 'PUT' && new URL(request.url).pathname.startsWith('/upload/')) {
const filename = new URL(request.url).pathname.replace('/upload/', '');
const object = await env.UPLOADS.put(filename, request.body, {
httpMetadata: { contentType: request.headers.get('content-type') || 'application/octet-stream' }
});
return Response.json({ key: object.key, size: object.size }, { status: 201 });
}
return new Response('Method Not Allowed', { status: 405 });
}
} satisfies ExportedHandler<Env>;
Anti-Patterns
Wrong: Destructuring ctx (loses this binding)
// ❌ BAD — causes "Illegal invocation"
const { waitUntil } = ctx;
waitUntil(somePromise);
Correct: Call methods on ctx directly
// ✅ GOOD — keep ctx intact
ctx.waitUntil(somePromise);
Wrong: Using Service Worker syntax
// ❌ BAD — legacy format
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
Correct: Use ES Module syntax
// ✅ GOOD — module format with typed env
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return new Response('Hello');
}
} satisfies ExportedHandler<Env>;
Wrong: Secrets in wrangler.toml
# ❌ BAD — visible in version control
[vars]
API_KEY = "sk-live-abc123"
Correct: Use wrangler secret
# ✅ GOOD — encrypted, never in code
npx wrangler secret put API_KEY
Wrong: Blocking response for background work
// ❌ BAD — user waits for analytics write
const data = await getData(env);
await env.DB.prepare('INSERT INTO analytics ...').run();
return Response.json(data);
Correct: Use ctx.waitUntil()
// ✅ GOOD — response returns immediately
const data = await getData(env);
ctx.waitUntil(env.DB.prepare('INSERT INTO analytics ...').run());
return Response.json(data);
Common Pitfalls
- Exceeding 1,000 subrequests: Each KV get, R2 read, D1 query, and fetch() counts. Fix: batch operations, use
Promise.all(). [src3] - KV eventual consistency: Writes take up to 60 seconds to propagate globally. Fix: use D1 or Durable Objects for strong consistency. [src7]
- Missing compatibility_date: Without it, runtime defaults to oldest behavior. Fix: set
compatibility_dateto today's date. [src1] - Node.js APIs unavailable: Buffer, crypto are unavailable by default. Fix: add
compatibility_flags = ["nodejs_compat_v2"]. [src4] - Large bundle size: Including unnecessary deps. Fix: use tree-shakeable ESM imports, check with
--dry-run --outdir dist. [src3] - D1 batch timeouts: Single UPDATE on 100K+ rows exceeds limits. Fix: batch in chunks of 1,000. [src6]
Diagnostic Commands
# Check Wrangler version and auth
npx wrangler --version && npx wrangler whoami
# Start local dev with real bindings
npx wrangler dev --remote
# Tail production logs
npx wrangler tail
# List deployments
npx wrangler deployments list
# Check KV contents
npx wrangler kv key list --namespace-id=YOUR_ID
# Run D1 query
npx wrangler d1 execute my-db --command "SELECT count(*) FROM items"
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Wrangler v3 | Current | Module Workers default, new config | Migrate from Service Worker syntax |
| Wrangler v2 | Deprecated | — | Run npx wrangler@3 init |
| nodejs_compat_v2 | Current | Replaces nodejs_compat | Better polyfill coverage |
| D1 | GA (2024) | — | Production-ready |
| R2 | GA (2023) | — | S3-compatible API |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Low-latency edge compute (<50 ms) | Long-running tasks >30 s CPU | AWS Lambda, Cloud Run |
| Global distribution (300+ PoPs) | Heavy CPU (ML inference) | GPU cloud functions |
| Simple KV or SQL storage | Large relational DB (>10 GB) | Managed PostgreSQL |
| File storage with no egress | POSIX filesystem needed | EC2, Cloud VMs |
| Cost-sensitive (<$5/mo) | WebSocket >30 min | Durable Objects |
Important Caveats
compatibility_dateis a one-way ratchet — once advanced, you cannot go back without potentially breaking changes.- Workers cannot make outbound connections to non-standard ports — only 80 and 443 for HTTP/HTTPS.
- KV
list()operations are eventually consistent — newly created keys may not appear immediately. - D1 is single-region (not globally replicated like KV) — use Smart Placement to co-locate Worker with D1.