Wrangler CLI (Cloudflare)
Table of Contents
- Wrangler CLI (Cloudflare) Cheatsheet
- 1. Setup & Authentication
- 2. Workers Development
- 3. Workers Configuration — wrangler.toml Deep Dive
- 4. KV (Key-Value Store)
- 5. R2 (Object Storage)
- 6. D1 (SQL Database)
- 7. Durable Objects
- 8. Pages
- 9. Queues
- 10. Secrets & Environment Variables
- 11. Cron Triggers & Scheduled Workers
- 12. Debugging & Monitoring
- 13. Advanced Patterns
- Quick Reference
Wrangler CLI (Cloudflare) Cheatsheet
Audience: DevOps engineers and developers, beginner to senior. Wrangler is the official CLI for building, testing, and deploying Cloudflare Workers, Pages, KV, R2, D1, Durable Objects, Queues, and more. Every section moves from basics to production-grade patterns.
1. Setup & Authentication
Installation
# Install globally (npm or pnpm)
npm install -g wrangler
pnpm add -g wrangler
# Install as devDependency (recommended for projects — pins version)
npm install --save-dev wrangler
npx wrangler --version
# Check version
wrangler --version
Authentication
# OAuth login (opens browser — best for local dev)
wrangler login
# Log out
wrangler logout
# Show authenticated account info
wrangler whoami
# Use API token instead of OAuth (CI/CD, automated pipelines)
export CLOUDFLARE_API_TOKEN=your_token_here
wrangler whoami # validates token
Creating an API Token (dashboard.cloudflare.com → My Profile → API Tokens):
- Use “Edit Cloudflare Workers” template for Workers deployment
- Scope to specific account/zone for least privilege
- API token is preferred over Global API Key in production
Senior tip: Never commit API tokens. Use
CLOUDFLARE_API_TOKENenv var in CI/CD. Rotate tokens quarterly. Use scoped tokens per project — if one leaks, blast radius is limited.
Gotcha: OAuth login stores credentials in
~/.config/.wrangler/. On shared CI runners, always use API token env vars — never login interactively.
wrangler.toml Anatomy
# Required fields
name = "my-worker" # Worker name (unique per account)
main = "src/index.ts" # Entry point
compatibility_date = "2024-11-01" # Locks behavior to a specific date
account_id = "abc123..." # Your Cloudflare account ID
# Optional: target specific zones/routes
routes = [
{ pattern = "example.com/api/*", zone_name = "example.com" }
]
# Optional: custom domains (no route matching overhead)
[[routes]]
pattern = "api.example.com/*"
custom_domain = true
# Build configuration
[build]
command = "npm run build"
cwd = "."
watch_dir = "src"
# Node.js compatibility (for npm packages using Node APIs)
node_compat = true
# Compatibility flags (enables specific behaviors)
compatibility_flags = ["nodejs_compat", "streams_enable_constructors"]
# Workers Unbound (no 10ms CPU limit — billed per duration)
[limits]
cpu_ms = 30000
Why compatibility_date matters: Cloudflare ships breaking runtime fixes but gates them behind this date. Setting it to today ensures you get fixes, but may break existing behavior. Test before bumping. Set it once, test thoroughly, then bump on major upgrades.
Environments
# Default environment (production)
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-11-01"
[vars]
API_URL = "https://api.example.com"
# Staging environment
[env.staging]
name = "my-worker-staging"
vars = { API_URL = "https://staging-api.example.com" }
route = "staging.example.com/*"
# Preview environment
[env.preview]
name = "my-worker-preview"
# Deploy to staging
wrangler deploy --env staging
# Tail logs from staging
wrangler tail --env staging
# Run dev against staging bindings
wrangler dev --env staging --remote
2. Workers Development
wrangler init
# Create new Worker project (interactive)
wrangler init my-worker
# Non-interactive (no git, no tests)
wrangler init my-worker --no-delegate-c3
# Use Cloudflare's create-cloudflare CLI (recommended for full project scaffolding)
npm create cloudflare@latest my-worker
wrangler dev — Local Development
# Start local dev server (default: localhost:8787)
wrangler dev
# Specify port and IP
wrangler dev --port 3000 --ip 0.0.0.0
# Local mode — runs entirely locally via Miniflare (no Cloudflare network)
wrangler dev --local
# Remote mode — proxies to Cloudflare's edge (uses real bindings, real KV/D1)
wrangler dev --remote
# Test against staging environment
wrangler dev --env staging --remote
# Live reload on save (default: on)
wrangler dev --watch
# Expose local dev server publicly via quick tunnel
wrangler dev --local-protocol https
Local vs Remote dev:
--localis faster but uses Miniflare emulation (may differ from production).--remoteuses real Cloudflare infrastructure but slower feedback loop and counts against rate limits. Use--localfor TDD,--remotefor integration testing.
Gotcha:
--localdoes not support all bindings (e.g., Durable Objects with persistence, AI binding). If your worker uses these, use--remoteor the--persist-toflag for local persistence.
wrangler deploy
# Deploy to production
wrangler deploy
# Deploy specific environment
wrangler deploy --env staging
# Deploy without publishing (dry-run shows what would be deployed)
wrangler deploy --dry-run
# Deploy with specific compatibility date
wrangler deploy --compatibility-date 2024-11-01
# Force deploy even if no changes (re-deploys same code)
wrangler deploy --dispatch-namespace my-namespace
# Upload source maps (for better error traces in dashboard)
wrangler deploy --upload-source-maps
wrangler delete
# Delete a Worker
wrangler delete
# Delete specific environment
wrangler delete --env staging
# Delete by name
wrangler delete --name my-worker
wrangler tail — Real-time Log Streaming
# Stream logs from production worker
wrangler tail
# Stream from specific environment
wrangler tail --env staging
# Output as JSON (for piping to jq or log aggregators)
wrangler tail --format json
# Filter by HTTP status code
wrangler tail --status 500
wrangler tail --status 200
# Filter by HTTP method
wrangler tail --method POST
# Filter by IP address
wrangler tail --ip 1.2.3.4
# Filter by search term in log body
wrangler tail --search "error"
# Combine filters
wrangler tail --status 500 --format json | jq '.exceptions[]'
# Set sampling rate (0-1, default 1 = 100%)
wrangler tail --sampling-rate 0.1
Production note:
wrangler tailuses Workers Logpush under the hood. In high-traffic scenarios, tail sampling (–sampling-rate) prevents overwhelming your terminal. For permanent log storage, configure Logpush to R2/S3 in the dashboard.
wrangler versions & rollback
# List deployed versions
wrangler versions list
# View specific version
wrangler versions view <version-id>
# Rollback to previous version
wrangler rollback
# Rollback to specific version
wrangler rollback <version-id>
# Gradual rollout (versions traffic splitting)
wrangler versions upload # Upload without deploying
wrangler deployments create # Create deployment with traffic %
Senior tip: Use
wrangler versions upload+wrangler deployments createfor canary deployments. Route 5% of traffic to new version, monitor withwrangler tail, then gradually increase. This is zero-downtime deployment at the edge.
3. Workers Configuration — wrangler.toml Deep Dive
Bindings Overview
Bindings connect your Worker to Cloudflare services. They appear as properties on the env object in your Worker code.
# KV Namespace binding
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123..."
preview_id = "xyz789..." # Used during wrangler dev
# R2 Bucket binding
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
preview_bucket_name = "my-bucket-preview"
# D1 Database binding
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "abc123..."
# Durable Objects binding
[[durable_objects.bindings]]
name = "MY_DO"
class_name = "MyDurableObject"
# Service binding (call another Worker)
[[services]]
binding = "AUTH_SERVICE"
service = "auth-worker"
environment = "production"
# Queue Producer binding
[[queues.producers]]
binding = "MY_QUEUE"
queue = "my-queue"
# Queue Consumer
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 10
max_batch_timeout = 5
max_retries = 3
dead_letter_queue = "my-dlq"
# Vectorize index
[[vectorize]]
binding = "MY_VECTORIZE"
index_name = "my-index"
# Workers AI
[ai]
binding = "AI"
# Hyperdrive (accelerated access to regional DBs)
[[hyperdrive]]
binding = "MY_HYPERDRIVE"
id = "abc123..."
Routes vs Custom Domains
# Route pattern (zone must be in your account)
[[routes]]
pattern = "example.com/api/*"
zone_name = "example.com"
# or zone_id = "abc123..."
# Custom domain (Workers takes over entire hostname)
[[routes]]
pattern = "api.example.com/*"
custom_domain = true
| Feature | Routes | Custom Domains |
|---|---|---|
| Pattern matching | Wildcard paths | Entire hostname |
| SSL cert | Uses zone cert | Cloudflare manages |
| Overhead | Route evaluation cost | None |
| Use case | Partial zone coverage | Dedicated API hostnames |
Cron Triggers
[triggers]
crons = ["0 * * * *", "*/5 * * * *"] # Hourly + every 5 mins
// Worker handler
export default {
scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(doWork(env));
}
};
4. KV (Key-Value Store)
Namespace Management
# Create namespace
wrangler kv namespace create MY_KV
# Create preview namespace (for local dev)
wrangler kv namespace create MY_KV --preview
# List all namespaces
wrangler kv namespace list
# Delete namespace
wrangler kv namespace delete --namespace-id abc123
Key Operations
# Put a value
wrangler kv key put --namespace-id abc123 "user:123" '{"name":"Alice"}'
# Put with TTL (seconds, min 60)
wrangler kv key put --namespace-id abc123 "session:xyz" "token" --ttl 3600
# Put with expiration (Unix timestamp)
wrangler kv key put --namespace-id abc123 "cache:page" "<html>" --expiration 1735689600
# Put with metadata
wrangler kv key put --namespace-id abc123 "key" "value" --metadata '{"source":"api"}'
# Get a value
wrangler kv key get --namespace-id abc123 "user:123"
# List keys (default: 1000)
wrangler kv key list --namespace-id abc123
# List with prefix filter
wrangler kv key list --namespace-id abc123 --prefix "user:"
# Delete a key
wrangler kv key delete --namespace-id abc123 "user:123"
# Use binding name instead of namespace-id (requires wrangler.toml)
wrangler kv key put --binding MY_KV "key" "value"
Bulk Operations
# Bulk put from JSON file (array of {key, value, expiration?, metadata?})
wrangler kv bulk put --namespace-id abc123 ./data.json
# data.json format:
# [
# { "key": "k1", "value": "v1" },
# { "key": "k2", "value": "v2", "expiration_ttl": 3600 }
# ]
# Bulk delete from JSON file (array of key strings)
wrangler kv bulk delete --namespace-id abc123 ./keys.json
Gotcha: KV is eventually consistent — writes propagate globally in ~60 seconds. Do not use KV for data requiring immediate global consistency (e.g., counters, locks). Use Durable Objects or D1 for strong consistency.
Senior tip: KV is optimized for high-read, low-write workloads. Cache TTLs at the Worker level (using
cacheTimeinkv.get()) to avoid per-request KV reads and reduce latency.
KV wrangler.toml binding
[[kv_namespaces]]
binding = "CACHE"
id = "abc123..." # Production namespace ID
preview_id = "xyz789..." # Preview namespace (wrangler dev uses this)
// Worker usage
const value = await env.CACHE.get("key");
await env.CACHE.put("key", "value", { expirationTtl: 3600 });
5. R2 (Object Storage)
Bucket Management
# Create bucket
wrangler r2 bucket create my-bucket
# Create with location hint (for data residency)
wrangler r2 bucket create my-bucket --location eu
# List buckets
wrangler r2 bucket list
# Delete bucket (must be empty)
wrangler r2 bucket delete my-bucket
# Enable public access (serves objects at r2.dev subdomain)
wrangler r2 bucket catalog enable my-bucket
# CORS configuration
wrangler r2 bucket cors set my-bucket --rules '[{"allowedOrigins":["*"],"allowedMethods":["GET"]}]'
Object Operations
# Upload object
wrangler r2 object put my-bucket/path/to/file.jpg --file ./local-file.jpg
# Upload with content type
wrangler r2 object put my-bucket/image.jpg \
--file ./image.jpg \
--content-type "image/jpeg"
# Upload with custom metadata
wrangler r2 object put my-bucket/file.json \
--file ./data.json \
--content-type "application/json" \
--metadata '{"source":"api","version":"2"}'
# Download object
wrangler r2 object get my-bucket/path/to/file.jpg --file ./output.jpg
# Delete object
wrangler r2 object delete my-bucket/path/to/file.jpg
Production note: R2 is S3-compatible. You can use any S3 SDK (
@aws-sdk/client-s3) with R2’s S3 endpoint:https://<account_id>.r2.cloudflarestorage.com. Use Workers binding for internal access; use S3 API for external tooling (Terraform, aws CLI).
Senior tip: Enable public bucket access for static assets with CloudFront/Cloudflare CDN in front. For private buckets, generate presigned URLs in your Worker using the
r2.createPresignedUrl()method — never expose bucket credentials to clients.
R2 wrangler.toml binding
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-assets-bucket"
preview_bucket_name = "my-assets-preview" # Local dev bucket
// Worker usage
const obj = await env.ASSETS.get("images/photo.jpg");
await env.ASSETS.put("uploads/file.pdf", body, {
httpMetadata: { contentType: "application/pdf" },
customMetadata: { userId: "123" }
});
// List objects
const list = await env.ASSETS.list({ prefix: "uploads/", limit: 100 });
// Delete
await env.ASSETS.delete("uploads/old-file.pdf");
6. D1 (SQL Database)
Database Management
# Create database
wrangler d1 create my-database
# List databases
wrangler d1 list
# Delete database
wrangler d1 delete my-database
# Get database info
wrangler d1 info my-database
Executing SQL
# Run inline SQL command (production)
wrangler d1 execute my-database --command "SELECT * FROM users LIMIT 10"
# Run SQL file (production)
wrangler d1 execute my-database --file ./schema.sql
# Run locally (requires --local flag)
wrangler d1 execute my-database --local --command "SELECT COUNT(*) FROM users"
# Run SQL file locally
wrangler d1 execute my-database --local --file ./seed.sql
# Batch execute (multiple statements)
wrangler d1 execute my-database --file ./migration.sql --batch-size 100
Migrations
# Create migration file (creates ./migrations/0001_init.sql)
wrangler d1 migrations create my-database "init"
# Apply all pending migrations (production)
wrangler d1 migrations apply my-database
# Apply locally
wrangler d1 migrations apply my-database --local
# List migration status
wrangler d1 migrations list my-database
# List local migration status
wrangler d1 migrations list my-database --local
-- migrations/0001_init.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
Senior tip: Always apply migrations
--localfirst, verify withwrangler d1 execute --local, then apply to production. D1 uses SQLite — AUTOINCREMENT is expensive; preferid INTEGER PRIMARY KEYwithout AUTOINCREMENT for performance-critical tables, using INSERT OR IGNORE patterns instead.
D1 Time Travel (Point-in-Time Restore)
# Export database (backup)
wrangler d1 export my-database --output ./backup.sql
# Export specific table
wrangler d1 export my-database --table users --output ./users.sql
# Restore using time-travel (within retention window)
wrangler d1 time-travel restore my-database --bookmark <bookmark-id>
# List bookmarks for time-travel
wrangler d1 time-travel info my-database
D1 wrangler.toml binding
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "abc123-def456..."
migrations_dir = "./migrations" # Optional, default: migrations/
// Worker usage
const result = await env.DB.prepare(
"SELECT * FROM users WHERE email = ?"
).bind(email).first();
// Batch queries (atomic)
const [user, posts] = await env.DB.batch([
env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(userId),
env.DB.prepare("SELECT * FROM posts WHERE user_id = ?").bind(userId),
]);
7. Durable Objects
Configuration in wrangler.toml
# Class definition (where the DO class lives)
[[durable_objects.bindings]]
name = "COUNTER" # Binding name in Worker env
class_name = "Counter" # Exported class name from Worker
# script_name = "other-worker" # Optional: class from another Worker
# Migrations (required when adding/changing DO classes)
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
# Renaming
[[migrations]]
tag = "v2"
renamed_classes = [{ from = "Counter", to = "CounterV2" }]
# Deleting
[[migrations]]
tag = "v3"
deleted_classes = ["OldCounter"]
Gotcha: Migrations are required when you first define a DO class or change class names. Without migrations, Wrangler will error on deploy. The
tagmust be unique and sequential — never reuse or reorder tags.
Durable Objects Worker Pattern
export class Counter implements DurableObject {
state: DurableObjectState;
env: Env;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
this.env = env;
}
async fetch(request: Request): Promise<Response> {
const count = (await this.state.storage.get<number>("count")) ?? 0;
await this.state.storage.put("count", count + 1);
return new Response(String(count + 1));
}
// Alarm pattern (scheduled task per DO instance)
async alarm() {
await doPeriodicWork(this.state, this.env);
// Re-schedule
await this.state.storage.setAlarm(Date.now() + 60_000);
}
}
export default {
async fetch(request: Request, env: Env) {
const id = env.COUNTER.idFromName("global"); // Deterministic ID
const stub = env.COUNTER.get(id);
return stub.fetch(request);
}
};
Senior tip: Use
idFromName()for predictable DO instances (e.g.,idFromName(userId)to shard by user). UsenewUniqueId()for ephemeral instances. DOs provide strongly consistent storage — ideal for rate limiters, sessions, game state, and coordination primitives.
8. Pages
Project Management
# Create Pages project
wrangler pages project create my-site
# List projects
wrangler pages project list
# Delete project
wrangler pages project delete my-site
Deployment
# Deploy build output
wrangler pages deploy ./dist
# Deploy to specific project
wrangler pages deploy ./dist --project-name my-site
# Deploy to specific branch (creates preview deployment)
wrangler pages deploy ./dist --project-name my-site --branch feature/new-ui
# Deploy with commit message
wrangler pages deploy ./dist --project-name my-site --commit-message "feat: add new landing page"
# Set custom build config (usually done via dashboard or direct-upload)
wrangler pages deploy ./dist --project-name my-site --commit-dirty
Pages vs Workers: Pages is optimized for frontend frameworks (Next.js, SvelteKit, Astro, etc.) with a built-in CI/CD pipeline tied to Git. Workers is for pure API/function workloads. Pages Functions (files in
/functionsdir) compile to Workers under the hood.
Pages Functions
# Functions live in /functions directory
# /functions/api/users.ts → handles /api/users
# /functions/api/[id].ts → handles /api/:id (dynamic route)
# Deploy with functions (automatic if /functions dir exists)
wrangler pages deploy ./dist --project-name my-site
# Local dev with functions
wrangler pages dev ./dist
# Local dev with specific port
wrangler pages dev ./dist --port 3000
// functions/api/users.ts
import type { EventContext } from "@cloudflare/workers-types";
export async function onRequestGet(ctx: EventContext<Env, any, any>) {
const users = await ctx.env.DB.prepare("SELECT * FROM users").all();
return Response.json(users.results);
}
_redirects and _headers
# /dist/_redirects — static redirects/rewrites
/old-path /new-path 301
/api/* https://api.example.com/:splat 200 # Proxy (rewrite)
# /dist/_headers — custom response headers
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'
/assets/*
Cache-Control: public, max-age=31536000, immutable
9. Queues
Queue Management
# Create queue
wrangler queues create my-queue
# List queues
wrangler queues list
# Delete queue
wrangler queues delete my-queue
# Add consumer (Worker that processes messages)
wrangler queues consumer add my-queue my-consumer-worker
# Remove consumer
wrangler queues consumer remove my-queue my-consumer-worker
# List consumers
wrangler queues consumer list my-queue
Producer + Consumer wrangler.toml
# Producer binding (push messages)
[[queues.producers]]
binding = "MY_QUEUE"
queue = "my-queue"
# Consumer binding (pull/process messages)
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 10 # Messages per batch (default: 10, max: 100)
max_batch_timeout = 5 # Seconds to wait for full batch
max_retries = 3 # Retry failed messages
dead_letter_queue = "my-dlq" # Route failed messages here
max_concurrency = 5 # Parallel consumer invocations
// Producer Worker
export default {
async fetch(request: Request, env: Env) {
await env.MY_QUEUE.send({ type: "email", userId: "123" });
// Batch send
await env.MY_QUEUE.sendBatch([
{ body: { type: "email", userId: "123" } },
{ body: { type: "sms", userId: "456" }, delaySeconds: 60 },
]);
return new Response("Queued");
}
};
// Consumer Worker
export default {
async queue(batch: MessageBatch<QueueMessage>, env: Env) {
for (const message of batch.messages) {
try {
await processMessage(message.body, env);
message.ack(); // Explicit ack (or use batch.ackAll())
} catch (e) {
message.retry({ delaySeconds: 30 }); // Retry with backoff
}
}
}
};
Senior tip: Use
delaySecondson messages for retry backoff patterns without a DLQ. Setmax_batch_timeoutlow (1-2s) for latency-sensitive processing; set it high (30s) for throughput-optimized batch workloads. Dead letter queues are essential in production — always configure them.
10. Secrets & Environment Variables
Secrets Management
# Add secret (prompts for value interactively)
wrangler secret put MY_API_KEY
# List secrets (values are never shown)
wrangler secret list
# Delete secret
wrangler secret delete MY_API_KEY
# Bulk set secrets from JSON
echo '{"KEY1":"val1","KEY2":"val2"}' | wrangler secret bulk
# Set secret for specific environment
wrangler secret put MY_API_KEY --env staging
.dev.vars — Local Development Secrets
# .dev.vars (gitignored, equivalent of .env for Workers)
MY_API_KEY=local-dev-key-here
DATABASE_URL=postgres://localhost/mydb
JWT_SECRET=dev-secret-not-for-production
Gotcha:
.dev.varsis forwrangler devonly. It is NEVER deployed. Production secrets must be set withwrangler secret put. Always add.dev.varsto.gitignore.
[vars] in wrangler.toml
# Non-secret environment variables (committed to source control)
[vars]
ENVIRONMENT = "production"
API_VERSION = "v2"
LOG_LEVEL = "error"
MAX_RETRY = "3"
[env.staging.vars]
ENVIRONMENT = "staging"
LOG_LEVEL = "debug"
| Feature | [vars] | wrangler secret | .dev.vars |
|---|---|---|---|
| Committed to git | Yes | No | No |
| Visible in dashboard | Yes | No (encrypted) | No |
| Use for | Non-sensitive config | API keys, tokens | Local dev only |
| Per-environment | Yes | Yes (–env flag) | No |
Senior tip: Rotate secrets by putting a new value — it deploys instantly at the edge. For zero-downtime rotation, set the new secret, deploy updated code that accepts both old and new (transition period), then delete the old secret.
11. Cron Triggers & Scheduled Workers
Configuration
[triggers]
crons = [
"0 * * * *", # Every hour at :00
"0 9 * * 1", # Every Monday at 9:00 UTC
"*/15 * * * *", # Every 15 minutes
"0 0 1 * *", # First day of every month
]
Worker Handler
export interface Env {
DB: D1Database;
}
export default {
// HTTP handler
async fetch(request: Request, env: Env): Promise<Response> {
return new Response("OK");
},
// Scheduled handler
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
// event.cron — the cron expression that triggered this
// event.scheduledTime — Unix timestamp
switch (event.cron) {
case "0 * * * *":
ctx.waitUntil(runHourlyCleanup(env));
break;
case "0 9 * * 1":
ctx.waitUntil(sendWeeklyReport(env));
break;
}
}
};
Testing Cron Locally
# Trigger scheduled event manually in local dev
curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"
Gotcha:
ctx.waitUntil()is critical for async cron work. Without it, the Worker may terminate before async tasks complete. Maximum execution time for scheduled Workers is 30 seconds (Bundled) or 15 minutes (Unbound).
12. Debugging & Monitoring
wrangler tail Advanced Usage
# Stream errors only
wrangler tail --status 500 --format json | jq '{
url: .event.request.url,
error: .exceptions[0].message,
timestamp: .eventTimestamp
}'
# Monitor POST requests
wrangler tail --method POST --method PUT
# Real-time log from specific IP
wrangler tail --ip 203.0.113.0
# All logs as pretty JSON
wrangler tail --format pretty
# Count error rate (last 100 events)
wrangler tail --status 500 --format json | head -100 | wc -l
Local Debugging
# Enable verbose output
wrangler dev --log-level debug
# Inspector URL (Chrome DevTools)
wrangler dev # Opens inspector at http://localhost:9229
# Use --inspect flag for explicit DevTools
wrangler dev --inspect
Open chrome://inspect in Chrome → “Configure” → add localhost:9229 → inspect your Worker with full devtools (breakpoints, call stack, variable inspection).
Miniflare (Unit Testing)
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "miniflare",
environmentOptions: {
kvNamespaces: ["MY_KV"],
d1Databases: ["MY_DB"],
},
},
});
// worker.test.ts
import { env } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import worker from "./src/index";
describe("Worker", () => {
it("returns 200", async () => {
const response = await worker.fetch(
new Request("http://localhost/"),
env
);
expect(response.status).toBe(200);
});
});
Senior tip: Always write tests with Miniflare/Vitest for Workers logic.
wrangler devis great for manual testing but CI requiresvitestwith@cloudflare/vitest-pool-workers. This enables true unit testing of Workers bindings without network calls.
13. Advanced Patterns
Multi-Worker Projects (Service Bindings)
# api-worker/wrangler.toml
name = "api-worker"
main = "src/index.ts"
compatibility_date = "2024-11-01"
[[services]]
binding = "AUTH"
service = "auth-worker" # Must be deployed first
environment = "production"
// api-worker: call auth-worker directly (no network hop)
const authResponse = await env.AUTH.fetch(
new Request("https://auth/verify", {
headers: { Authorization: request.headers.get("Authorization") ?? "" }
})
);
Service bindings bypass the internet — Worker-to-Worker calls over service bindings are in-memory on the same machine, sub-millisecond latency, and don’t count as external requests. This is the Cloudflare microservices pattern.
Monorepo Setup
# packages/
# api-worker/wrangler.toml
# auth-worker/wrangler.toml
# shared/ ← shared utilities
# Deploy all workers from root
cd packages/api-worker && wrangler deploy
cd packages/auth-worker && wrangler deploy
# Or with turborepo/nx pipeline
turbo run deploy --filter=*-worker
CI/CD — GitHub Actions
# .github/workflows/deploy.yml
name: Deploy Workers
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
# Apply D1 migrations before deploy
- name: Apply D1 Migrations
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: d1 migrations apply my-database --env production
Production note: Always run D1 migrations before deploying code changes that depend on the new schema. In the GitHub Actions pipeline: migrations → deploy. Use
--dry-runin PR checks to validate wrangler.toml without deploying.
Custom Build Commands
[build]
command = "npm run build" # Build command
cwd = "." # Working directory
[build.upload]
format = "service-worker" # "service-worker" or "modules"
main = "./dist/worker.js"
Workers Assets (Static Site + API)
# Serve static assets from a directory alongside Worker code
assets = { directory = "./public", binding = "ASSETS" }
# Or with advanced config
[assets]
directory = "./dist"
binding = "ASSETS"
html_handling = "auto-trailing-slash"
not_found_handling = "single-page-application" # SPA mode: all 404s → index.html
// Worker can serve assets + handle API routes
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) {
return handleApi(request, env);
}
// Fall through to static asset serving
return env.ASSETS.fetch(request);
}
};
Gradual Migration v1 to v2 Workers
# 1. Upload new version without deploying
wrangler versions upload
# 2. Create split deployment (10% to new version)
wrangler deployments create \
--version-id <new-version-id> \
--version-percentage 10 \
--version-id <old-version-id> \
--version-percentage 90
# 3. Monitor with tail
wrangler tail --format json | jq '.scriptVersion'
# 4. Gradually shift traffic
# Repeat deployments create with 25%, 50%, 75%, 100%
Common wrangler.toml Patterns
# Full production worker example
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-11-01"
node_compat = true
[vars]
ENVIRONMENT = "production"
[[kv_namespaces]]
binding = "CACHE"
id = "prod-namespace-id"
preview_id = "dev-namespace-id"
[[d1_databases]]
binding = "DB"
database_name = "my-api-db"
database_id = "prod-db-id"
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "my-api-uploads"
[[queues.producers]]
binding = "JOBS"
queue = "background-jobs"
[[queues.consumers]]
queue = "background-jobs"
max_batch_size = 10
dead_letter_queue = "background-jobs-dlq"
[triggers]
crons = ["0 */4 * * *"]
[[migrations]]
tag = "v1"
new_classes = ["RateLimiter"]
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"
[env.staging]
name = "my-api-staging"
vars = { ENVIRONMENT = "staging" }
[env.staging.d1_databases]
[[env.staging.d1_databases]]
binding = "DB"
database_name = "my-api-db-staging"
database_id = "staging-db-id"
Quick Reference
Common Commands
| Task | Command |
|---|---|
| Login | wrangler login |
| Deploy production | wrangler deploy |
| Deploy staging | wrangler deploy --env staging |
| Local dev | wrangler dev |
| Remote dev | wrangler dev --remote |
| Stream logs | wrangler tail |
| Stream error logs | wrangler tail --status 500 |
| Rollback | wrangler rollback |
| KV put | wrangler kv key put --binding KV "key" "value" |
| KV get | wrangler kv key get --binding KV "key" |
| D1 query | wrangler d1 execute DB --command "SELECT 1" |
| D1 migrate | wrangler d1 migrations apply DB |
| R2 upload | wrangler r2 object put bucket/key --file ./file |
| Add secret | wrangler secret put SECRET_NAME |
Wrangler Environment Variables
| Variable | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN | API token (preferred in CI/CD) |
CLOUDFLARE_ACCOUNT_ID | Account ID override |
CLOUDFLARE_EMAIL | Account email (legacy) |
CLOUDFLARE_API_KEY | Global API key (legacy, avoid) |
WRANGLER_LOG | Log level: debug, info, warn, error |
WRANGLER_SEND_METRICS | Disable telemetry: false |
Compatibility Flags
| Flag | Purpose |
|---|---|
nodejs_compat | Enable Node.js API compatibility layer |
streams_enable_constructors | Enable WHATWG Streams constructors |
no_global_navigator | Remove navigator global (strict mode) |
formdata_parser_supports_files | Better FormData file handling |
Final senior note: Pin your
compatibility_dateandwranglerpackage version inpackage.json. Wrangler follows semver but Cloudflare runtime behavior changes are gated bycompatibility_date. Upgrade these intentionally, not silently via^ranges. A mismatch between local wrangler and CI wrangler has caused many production incidents.