← CLI Cheatsheets

Wrangler CLI (Cloudflare)

Table of Contents

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_TOKEN env 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: --local is faster but uses Miniflare emulation (may differ from production). --remote uses real Cloudflare infrastructure but slower feedback loop and counts against rate limits. Use --local for TDD, --remote for integration testing.

Gotcha: --local does not support all bindings (e.g., Durable Objects with persistence, AI binding). If your worker uses these, use --remote or the --persist-to flag 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 tail uses 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 create for canary deployments. Route 5% of traffic to new version, monitor with wrangler 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
FeatureRoutesCustom Domains
Pattern matchingWildcard pathsEntire hostname
SSL certUses zone certCloudflare manages
OverheadRoute evaluation costNone
Use casePartial zone coverageDedicated 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 cacheTime in kv.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 --local first, verify with wrangler d1 execute --local, then apply to production. D1 uses SQLite — AUTOINCREMENT is expensive; prefer id INTEGER PRIMARY KEY without 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 tag must 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). Use newUniqueId() 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 /functions dir) 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 delaySeconds on messages for retry backoff patterns without a DLQ. Set max_batch_timeout low (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.vars is for wrangler dev only. It is NEVER deployed. Production secrets must be set with wrangler secret put. Always add .dev.vars to .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 gitYesNoNo
Visible in dashboardYesNo (encrypted)No
Use forNon-sensitive configAPI keys, tokensLocal dev only
Per-environmentYesYes (–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 dev is great for manual testing but CI requires vitest with @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-run in 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

TaskCommand
Loginwrangler login
Deploy productionwrangler deploy
Deploy stagingwrangler deploy --env staging
Local devwrangler dev
Remote devwrangler dev --remote
Stream logswrangler tail
Stream error logswrangler tail --status 500
Rollbackwrangler rollback
KV putwrangler kv key put --binding KV "key" "value"
KV getwrangler kv key get --binding KV "key"
D1 querywrangler d1 execute DB --command "SELECT 1"
D1 migratewrangler d1 migrations apply DB
R2 uploadwrangler r2 object put bucket/key --file ./file
Add secretwrangler secret put SECRET_NAME

Wrangler Environment Variables

VariablePurpose
CLOUDFLARE_API_TOKENAPI token (preferred in CI/CD)
CLOUDFLARE_ACCOUNT_IDAccount ID override
CLOUDFLARE_EMAILAccount email (legacy)
CLOUDFLARE_API_KEYGlobal API key (legacy, avoid)
WRANGLER_LOGLog level: debug, info, warn, error
WRANGLER_SEND_METRICSDisable telemetry: false

Compatibility Flags

FlagPurpose
nodejs_compatEnable Node.js API compatibility layer
streams_enable_constructorsEnable WHATWG Streams constructors
no_global_navigatorRemove navigator global (strict mode)
formdata_parser_supports_filesBetter FormData file handling

Final senior note: Pin your compatibility_date and wrangler package version in package.json. Wrangler follows semver but Cloudflare runtime behavior changes are gated by compatibility_date. Upgrade these intentionally, not silently via ^ ranges. A mismatch between local wrangler and CI wrangler has caused many production incidents.