wrangler
Wrangler is the command line tool for Cloudflare Workers. It scaffolds a project, runs your Worker locally on workerd (the same runtime Cloudflare runs in production, driven through Miniflare), and pushes it live with wrangler deploy. Everything the Worker can touch is declared in one config file, wrangler.jsonc or wrangler.toml: the entry script, a compatibility date, and bindings for KV, R2, D1, Durable Objects, Queues, Hyperdrive, Vectorize, and the rest. Wrangler also carries the day-to-day operations commands for those products, so creating a D1 database, running a migration, putting a secret, streaming production logs, and rolling a new version out to a slice of traffic are all subcommands of the same binary. It lives in the cloudflare/workers-sdk monorepo alongside miniflare and the Vite plugin, and it pins the workerd build it ships with, so upgrading wrangler upgrades your local runtime too.
If you ship to Cloudflare, wrangler is not a choice you make, it is the platform, and the real workerd runtime plus generated binding types make it a good one. Pin it as a devDependency and actually read the changelog, because near-daily releases keep moving the CLI and the runtime underneath it at the same time.
Use it if
- You deploy to Cloudflare Workers or Pages at all: this is the only first-party deploy path, and the one Cloudflare tests its runtime releases against
- You want local development that matches production: wrangler dev runs real workerd rather than a Node shim, so an API the edge does not support fails on your laptop instead of after deploy
- You want typed bindings: wrangler types reads wrangler.jsonc and writes worker-configuration.d.ts with an Env interface plus the runtime globals, so TypeScript knows what env.CACHE actually is
- You need progressive rollouts or a fast undo: wrangler versions upload then wrangler versions deploy splits traffic between two versions, and wrangler rollback puts the previous one back
- You run CI: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID and every command runs non-interactively with no browser login
- You are not on Cloudflare. This is single-vendor tooling with no abstraction layer: the config file, the bindings model, and every subcommand assume a Cloudflare account
- You want a CLI that holds still. 32 releases went out in the 90 days to 2026-08-06, and workerd and miniflare are exact-pinned dependencies, so a wrangler patch bump swaps the runtime under your tests; 4.119.0 ships miniflare 5.20260801.0-alpha inside a stable release
- You are stuck on Node 20 or older. engines declares node >=22.0.0, so your CI image and your teammates all have to move first
- You want infrastructure as code. wrangler.jsonc declares bindings for one Worker, but the KV namespace, D1 database, and R2 bucket behind them are created by imperative commands whose ids you paste back into the file by hand, with no state file, no diff, and no destroy
- You are building a Vite app. Running wrangler dev next to a Vite dev server duplicates work that @cloudflare/vite-plugin already does in one process
- You expect local mode to be production. wrangler dev defaults to local, where KV, R2, and D1 are simulated in .wrangler/state, so geo headers, cache behavior, and any data that already exists in your account are all wrong until you add --remote
- You want a small tool. There are over 40 top-level command groups now, most of them for Cloudflare products you do not use, and wrangler --help is a wall of text
Setup reality
npm create cloudflare@latest scaffolds a project and pins wrangler as a devDependency, which is what you want, because a globally installed wrangler will drift from what CI runs. Two install requirements bite early: Node 22 or newer, and a platform-specific workerd binary pulled at install time, so Docker images need a matching libc and CI caches have to be per-platform. The first command opens a browser for OAuth; on a headless box use wrangler login --device or skip login entirely by exporting CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. Then the config catches you twice. compatibility_date is mandatory and freezes runtime behavior at that date, so a project left on a two-year-old date quietly keeps old semantics forever. And Node builtins throw at import until you add nodejs_compat to compatibility_flags. Finally, every binding in your config must point at a resource that already exists, which means a first-day round of wrangler kv namespace create and wrangler d1 create with ids copied into wrangler.jsonc by hand.
Patterns
Start a Worker and run it locallyscaffold-and-dev
npm create cloudflare@latest my-worker -- --type=hello-world
cd my-worker
npx wrangler dev # local workerd on http://localhost:8787
npx wrangler dev --remote # same code on the real edge, real bindings
npx wrangler dev --port 3000 --persist-to .cache/wranglerdev is local by default: workerd plus Miniflare's simulated KV, R2, and D1 under .wrangler/state. --remote runs on Cloudflare's network against production resources, which is the only way to see real geo headers, real cache behavior, or data that already exists in a namespace.
The wrangler.jsonc that everything readsconfig-file
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"compatibility_flags": ["nodejs_compat"],
"observability": { "enabled": true },
"vars": { "LOG_LEVEL": "info" },
"kv_namespaces": [
{ "binding": "CACHE", "id": "b1f0e2..." }
],
"d1_databases": [
{ "binding": "DB", "database_name": "app", "database_id": "9c2e41..." }
]
}compatibility_date is required and pins runtime behavior to that date, so an old date keeps old bug-for-bug semantics silently. Importing node:buffer or node:crypto throws until nodejs_compat is in compatibility_flags. The ids are real account resources, so this file cannot be committed with placeholders and still deploy.
Keep Env in sync with your bindingsgenerate-types
npx wrangler types # writes worker-configuration.d.ts
npx wrangler types --env-interface CloudflareEnv
npx wrangler types --include-runtime=false # Env only, no runtime globalsRuntime types are included by default now, which is why @cloudflare/workers-types is only an optional peer dependency. Rerun this every time you touch bindings or Env goes stale and TypeScript will happily let you read env.CACHE when there is no such binding at runtime. Put it in a predev and prebuild script instead of trusting memory.
Secrets in production, .dev.vars locallysecrets-and-local-vars
echo "sk_live_abc" | npx wrangler secret put STRIPE_KEY
npx wrangler secret list
npx wrangler secret bulk secrets.json
# .dev.vars (gitignored, only read by wrangler dev)
# STRIPE_KEY="sk_test_abc"
# DATABASE_URL="postgres://localhost/dev"Secrets set with wrangler secret live on Cloudflare and never touch your repo, but wrangler dev cannot see them; it reads .dev.vars instead, so a missing entry there shows up as undefined only in local runs. Anything under vars in wrangler.jsonc is plaintext in git and in the dashboard, so keep that for log levels and feature flags.
Create the KV, R2, and D1 resources your bindings point atcreate-resources
npx wrangler kv namespace create CACHE # prints the config snippet to paste
npx wrangler kv key put --binding=CACHE hello world --remote
npx wrangler r2 bucket create uploads
npx wrangler d1 create app
npx wrangler d1 execute app --remote --command "select name from sqlite_master"
npx wrangler d1 migrations apply app --remoteEvery create command prints a config block you paste into wrangler.jsonc yourself; nothing writes it back, and a wrong id fails at deploy rather than at edit time. Pass --local or --remote explicitly on kv key and d1 execute: the default sends you to .wrangler/state, and debugging rows that only exist on your laptop costs an afternoon.
Deploy, and see what would be deployed firstdeploy-and-dry-run
npx wrangler deploy
npx wrangler deploy --dry-run --outdir dist # bundle only, no upload, no auth
npx wrangler deploy --minify
npx wrangler whoami--dry-run --outdir is the fastest way to answer "why is my Worker over the size limit", because it writes the exact bundle esbuild produced without needing credentials. It is also the check to run on pull requests, since it catches a broken import or a missing binding without touching production.
Staging and production from one configenvironments
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"vars": { "LOG_LEVEL": "info" },
"env": {
"staging": {
"name": "my-worker-staging",
"vars": { "LOG_LEVEL": "debug" },
"kv_namespaces": [{ "binding": "CACHE", "id": "a44c..." }]
}
}
}Named environments do not inherit bindings from the top level. If staging declares any kv_namespaces it must declare all of them, and forgetting one gives you a Worker that deploys fine and then throws on the first request. vars behave the same way. Deploy with wrangler deploy --env staging and dev with wrangler dev --env staging.
Stream logs from a deployed Workertail-logs
npx wrangler tail
npx wrangler tail --format pretty --status error
npx wrangler tail --search "checkout" --method POST
npx wrangler tail --ip self --format json | jq '.logs[].message'tail is a live stream with no history, so it only shows what happens while you are attached; for anything after the fact you need observability enabled in the config and the dashboard or Logpush. Sampling kicks in on high-traffic Workers, so an absent log line does not prove the code path did not run.
Ship a version to a slice of trafficgradual-rollout
npx wrangler versions upload # uploads without serving traffic
npx wrangler versions list
npx wrangler versions deploy <new-id>@10% <old-id>@90%
# happy with it, take it to 100%
npx wrangler versions deploy <new-id>@100%wrangler deploy always goes straight to 100%, so gradual rollout is a different command pair entirely: upload then deploy. Percentages must add up to 100 and only two versions can be live at once. Durable Object migrations and some binding changes cannot be split this way and will be rejected at upload.
Undo a bad deployrollback
npx wrangler deployments list
npx wrangler deployments status
npx wrangler rollback --message "checkout 500s"
npx wrangler rollback <version-id>Rollback swaps the served version back, which is fast, but it does not touch anything outside the script: secrets you changed, D1 migrations you applied, and KV writes stay exactly as the bad deploy left them. Treat it as a way to stop the bleeding, not as an undo.
Scheduled Workers and how to test themcron-triggers
// wrangler.jsonc
{ "triggers": { "crons": ["0 * * * *", "*/15 * * * *"] } }
// src/index.ts
export default {
async scheduled(event: ScheduledController, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(env.DB.prepare("delete from sessions where expires_at < ?")
.bind(Date.now()).run())
}
} satisfies ExportedHandler<Env>Crons run in UTC, always, with no timezone option. Locally they do not fire on a schedule at all: start wrangler dev --test-scheduled and hit http://localhost:8787/__scheduled to invoke the handler by hand. Work started without ctx.waitUntil can be cut off when the handler returns.
Deploy from CI without a browser loginci-deploy
# .github/workflows/deploy.yml
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx wrangler deploy --dry-run --outdir dist
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}The API token needs the Workers Scripts edit permission plus one permission per product you bind to, and a token that works for deploy will still fail on wrangler d1 execute until you add D1 edit. Node 22 is not optional here. Add WRANGLER_SEND_METRICS=false if you do not want telemetry from build agents.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @cloudflare/vite-plugin | npm | You are building a Vite app for Workers and want one dev server for the frontend and the Worker instead of running wrangler dev beside it |
| miniflare | npm | You only want the local Workers runtime embedded in your own scripts or tests, without the deploy, auth, and account management commands |
| alchemy | npm | You want Cloudflare resources declared as code with state and diffs, rather than created imperatively with ids pasted into wrangler.jsonc |
| vercel | npm | Your app is Next.js shaped and you would rather keep a mature framework deploy story than port everything onto Workers bindings |