How to Debug Cloudflare Workers Locally with Wrangler Dev
Deploy-and-pray debugging is expensive on Workers. Each iteration is a 20-second build plus a round trip, and console output from production is easy to lose. Local mode fixes most of that, but only if you know what it does not emulate.
Direct answer: run npx wrangler dev in the project root to start Cloudflare Workers locally on port 8787 with workerd. Add --remote to run against production bindings such as D1, KV, and AI. Use npx wrangler tail for live production logs, filtered with --status error.
What does local mode actually run?
It runs workerd, the same open-source runtime that powers production. That is a real fidelity win over miniflare 2 — timers, streams, and the fetch API behave the same.
What differs: the network path. Locally your subrequests go straight out from your machine, so anything depending on Cloudflare’s colo routing, caching layers, or origin rules will not behave the same. Latency numbers from local dev are meaningless.
Start with the basics:
npx wrangler dev --port 8787
You get a local HTTP server and hot reload on file save. Bindings declared in wrangler.jsonc are simulated with local storage in .wrangler/state.
How do you deal with bindings that need real data?
Local simulacra start empty. A D1 query against an empty local database returns zeros and looks like a broken query.
Three options:
npx wrangler d1 execute mydb --local --file=./schema.sql
That applies your schema to local storage. Swap --local for --remote to run against the real bindings.
--remote runs your code in Cloudflare’s network but keeps the dev loop. It is slower per request and it touches real data, which is dangerous — a test that deletes records will delete real records. Use a staging namespace.
The third option is seeding: a seed.sql committed to the repo, applied by an npm script. For team work this is the only approach that gives everyone the same starting state.
How do you see what production is doing?
wrangler tail streams logs from deployed workers in real time:
npx wrangler tail my-worker --format pretty --status error
--status error filters to failing requests. Without it, the volume on a busy worker makes the stream useless within seconds.
For the AI binding or slow subrequests, add --sampling-rate 0.1 to sample 10% rather than everything. Sampling keeps the stream readable and does not meaningfully change what you find.
The other tool is wrangler dev --inspect, which attaches a Chrome DevTools debugger. Breakpoints in a Worker are genuinely useful for inspecting env and request objects. Set --inspector-port 9229 if the default collides.
What errors only appear at the edge?
Several, and they are the ones that cost hours.
CPU time exceeded. Local runs have no CPU budget. A 400 ms JSON transformation passes locally and throws 1102 in production under the 10 ms free-plan limit or the 30 s paid default.
Subrequest limits. Free plan allows 50 subrequests per request, paid 1,000. A fan-out loop that calls 200 origins works locally and fails in production at request 51.
Memory. 128 MB per isolate. Holding a large response body in memory allocates across requests poorly.
Cold environment variables. A variable referenced but undefined returns undefined silently instead of throwing. Log Object.keys(env) once at startup during development.
How do you structure logs worth reading?
Do not use bare console.log. Emit JSON with a request ID:
console.log(JSON.stringify({
level: "error", rid: request.headers.get("cf-ray"),
msg: "db query failed", err: e.message
}));
cf-ray is Cloudflare’s request identifier and it appears in support tickets and error pages. Including it means a customer report maps directly to a log line.
Keep a console.log out of hot paths. Each one has a small cost and the volume adds up on high-traffic routes.
What is still painful
Unit testing a Worker needs @cloudflare/vitest-pool-workers, and it is more setup than a normal Vitest project because bindings must be declared in the test config. Budget an afternoon for the first one.
And there is no local emulation of edge caching behaviour. Cache API calls in local dev work against a local cache with its own rules. If your bug involves cache timing, you will only find it in production or on a staging deployment.
Key Takeaways
wrangler devruns the realworkerdruntime locally, so most behaviour matches production.- Local bindings start empty — apply a schema file or seed script, or use
--remotewith a staging namespace. wrangler tail --status erroris how you actually see production failures.- CPU time, subrequest counts, and 128 MB memory limits are enforced only in production.
- Log structured JSON including
cf-rayso customer reports map to log lines.
