The answer. wrangler dev runs your code in workerd on your machine and gives every binding a local simulation: KV, R2, D1 and Queues persist under .wrangler/state/v3/, Durable Objects run in-process, Cron Triggers fire on request. Nothing you write reaches Cloudflare. That is the right default, and it is also the trap behind "my write never happened": the same local-first rule applies to wrangler kv key put and wrangler r2 object put. Two escape hatches exist. Per binding, remote: true in the config makes that one binding (an R2 bucket, Workers AI, Email) talk to the real resource while everything else stays local. Per session, wrangler dev --remote runs the whole Worker in a sandbox on Cloudflare's network with real bindings.
The pattern.
{
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "media-staging", "remote": true }], // real bucket
"kv_namespaces": [{ "binding": "CACHE", "id": "<staging-id>" }] // still local
}
npx wrangler dev # local runtime, remote only where remote: true
npx wrangler dev --remote # everything on Cloudflare's network (needs auth, bills usage)
ls .wrangler/state/v3/ # kv/ r2/ d1/ — the proof a local write landed
Watch out.
- Workers AI has no local model runtime; even in local mode the binding calls the GPU fleet and bills you.
- Two Workers connected by a Service Binding discover each other when both run under
wrangler dev; older guides that say "deploy the callee first" predate that. .wrangler/is state, not source. Gitignore it, and delete the folder when a stale D1 schema confuses you.
Related: wrangler-kv-r2-default-local-use-remote · d1-migrations-prepared-statements-and-batch · first-worker-create-dev-deploy-in-five-commands