How to Debug Cloudflare Workers Errors: Logs, Traces and Error Codes
Explore with AI
Start with the error code the client sees. 1101 means your code threw a JavaScript exception, and 1102 means the Worker ran out of CPU time or memory. To find the failing line, turn on Workers Logs and source maps in your Wrangler config, stream live errors with `npx wrangler tail`, then replay the request under `wrangler dev`, where DevTools, breakpoints and local traces show you what broke.
On this page
- What the Workers error codes mean
- Turn on Workers Logs and source maps before the next error
- From error page to failing line, step by step
- Reproducing the error locally with DevTools, breakpoints and traces
- Runtime errors that only happen on Workers
- When a limit stops the Worker: error 1102
- Mistakes that hide Workers errors
- Letting Polylane watch your Workers for errors
- Common questions
Most Cloudflare Workers errors reach you in one of three ways: a numbered error page, a spike in the dashboard, or a stack trace that points at minified code. Debugging them follows a short loop. Read the code Cloudflare returns. Find the exception and its stack trace in Workers Logs or wrangler tail. Map the trace back to your source with source maps. Then replay the request under wrangler dev with DevTools attached.
The loop only works if the evidence exists when the error happens. So turn on logs and source maps first. Then learn the few runtime errors that only happen on Workers, because general JavaScript advice will not fix them.
What the Workers error codes mean
When a Worker in production fails before it returns a response, the client gets a Cloudflare error page with a code. The code is your first clue. It tells you whether your code threw, a limit stopped it, or the request never reached your code at all.
Workers error codes and where to look first
| Code | What happened | Where to look first |
|---|---|---|
| 1101 | Worker threw a JavaScript exception | Stack trace in logs |
| 1102 | Worker exceeded its CPU time or memory limit | Metrics, Invocation Statuses |
| 1103 | The Worker owner needs to contact Cloudflare Support | Cloudflare Support |
| 1019 | Worker hit the loop limit | Worker-to-Worker call chain |
| 1021 | Worker requested a host it cannot access | Outbound fetch target |
| 1022 | Cloudflare failed to route the request to the Worker | Routes and domains |
| 1024 | Subrequest to a Cloudflare-owned IP address | Outbound fetch target |
| 1027 | Free plan daily request limit exceeded | Plan and route fail mode |
| 1042 | Fetch to another Worker on the same zone without global_fetch_strictly_public | Compatibility flags |
| 10162 | Module has an unsupported Content-Type | Build output |
Two codes cover most incidents. 1101 means your JavaScript threw an exception, so the answer is in a stack trace. 1102 means the Worker exceeded its resource limits, either CPU time or memory, so the answer is in your metrics. Other 11xx codes usually point to a problem in the Workers runtime itself. Check the Cloudflare status page before you touch your code.
A 1027 has a twist. Free accounts get 100,000 requests a day, reset at midnight UTC. A route set to fail closed returns the 1027 page. A route set to fail open skips the Worker, so requests behave as if no Worker were configured, and you see odd behaviour with no error at all.
Turn on Workers Logs and source maps before the next error
Workers Logs collects invocation logs, your console.log output, errors and uncaught exceptions. It stores them in your Cloudflare account, where you can query them per Worker. New Workers have it on by default. Older Workers need the setting in the Wrangler file and a redeploy, and it needs Wrangler 3.78.6 or later.
// wrangler.jsonc
{
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
"upload_source_maps": true
}
head_sampling_rate runs from 0 to 1 and defaults to 1, which logs every request. upload_source_maps tells Wrangler to generate and upload source maps on wrangler deploy and wrangler versions deploy. Cloudflare then maps the stack trace of any uncaught exception back to your original files and lines, so a TypeScript error points at the .ts file you wrote. Source maps need Wrangler 3.46.0 or later, and each map can be up to 15 MB gzipped.
I built and led the Workers observability team at Cloudflare, and my advice on this step is blunt: a log you never collected cannot help you, so switch it on before the incident.
Log objects so you can filter by field
Workers Logs extracts the fields of any JSON object you log and indexes them.
// Hard to query: the ID is buried in a string
console.log("user_id: " + userId);
// Easy to query: user_id and order_id become fields
console.log({ user_id: userId, order_id: orderId, status: res.status });
The first line hides the ID inside a message, so finding every log for one user means a text search. The second gives you user_id and order_id as fields you can filter on in the middle of an incident.
What Workers Logs costs and how long it keeps data
Workers Logs is included on both plans. The Free plan writes 200,000 log events a day and keeps them for 3 days. The Paid plan includes 20 million events a month, charges $0.60 per additional million, and keeps them for 7 days. A single log can be up to 256 KB. Anything larger is truncated and marked with $cloudflare.truncated. For longer retention, send logs to another tool with OpenTelemetry export, Workers Logpush or Tail Workers.
From error page to failing line, step by step
- Read the code. Match the code on the error page, or the status your client got, against the table above. That tells you whether to hunt for an exception, a limit or a routing problem.
- Check the invocation statuses. In the dashboard, go to Workers & Pages, select the Worker, and open Metrics > Errors > Invocation Statuses.
Exceeded CPU Time LimitsandExceeded Memoryappear there as separate lines, so a 1102 turns into one of two specific problems. - Stream live errors. From the project directory, run:
npx wrangler tail
Each event arrives as a structured JSON object:
{
"outcome": "ok",
"scriptName": null,
"exceptions": [],
"logs": [],
"eventTimestamp": 1590680082349,
"event": {
"request": { "url": "https://www.bytesized.xyz/", "method": "GET", "headers": {}, "cf": {} }
}
}
Pipe it through jq to keep only the failed invocations:
npx wrangler tail | jq 'select(.outcome != "ok") | {outcome, exceptions, url: .event.request.url}'
The same stream is in the dashboard: open the Worker’s Logs tab and select Live.
- Search history in Workers Logs. If the error happened an hour ago, tail will not show it. Open the Worker in Workers & Pages and select Observability. Each invocation writes one invocation log, marked
$cloudflare.$metadata.type = "cf-worker-event", with the request, the response and metadata. Your own logs and exceptions sit next to it. - Read the remapped stack trace. With source maps uploaded, the stack trace in real-time logs and Tail Workers points at your source file and line. Cloudflare leaves any line it cannot remap as it was. A partly minified trace can mean the source map is stale or missing.
- Reproduce it locally. Once you know the route and the input, replay the request against
wrangler devand step through it, as the next section shows.
Reproducing the error locally with DevTools, breakpoints and traces
wrangler dev runs your Worker on your machine in workerd, the same open-source runtime that runs in production, and serves it on localhost:8787. Exceptions and console.log output print in the terminal. For errors in a Cron Trigger, wrangler dev --test-scheduled exposes a /cdn-cgi/local/scheduled route that fires the scheduled handler on demand.
Open DevTools
Press D in the terminal running wrangler dev and Cloudflare’s build of Chrome DevTools opens in a browser tab. With Vite and the Cloudflare Vite plugin, open the debug URL printed in the console, for example http://localhost:5173/__debug. DevTools gives you the console, breakpoints, CPU profiling and memory snapshots. The dashboard editor and the Workers Playground include DevTools too.
Set breakpoints in VS Code
Create .vscode/launch.json:
{
"configurations": [
{
"name": "Wrangler",
"type": "node",
"request": "attach",
"port": 9229,
"cwd": "/",
"resolveSourceMapLocations": null,
"attachExistingChildren": false,
"autoAttachChildProcesses": false
}
]
}
Run npx wrangler dev in the VS Code terminal, pick the Wrangler configuration in the Run & Debug panel and press play. Set a breakpoint, request http://127.0.0.1:8787, and execution stops on that line. WebStorm works the same way with an Attach to Node.js/Chrome configuration on port 9229. If that port is taken, wrangler dev --inspector-port sets another one.
Query local traces to see which call failed
Current versions of wrangler dev and vite dev capture OpenTelemetry traces for every local invocation, with no SDK and no code change. The runtime records spans for outbound fetch calls, for binding calls to KV, R2, D1, Durable Objects and Queues, and for the handler itself. Press e in Wrangler, or visit /cdn-cgi/explorer on the local server, to open the Local Explorer. Click a failing request to see its spans, errors and correlated logs.
This is the fastest way to see which operation broke. In Cloudflare’s own example, an order endpoint returned 500. The trace showed that the KV read succeeded, the D1 insert failed with no such column: delivery_window, and the Queue send never ran. The cause was a migration that had not been applied locally.
Coding agents get the same data. When Wrangler detects an agent session, it prints the address of the Local Explorer API, which serves an OpenAPI schema and a read-only endpoint for querying traces and logs with SQL. A plain prompt is enough:
Runtime errors that only happen on Workers
“The script will never generate a response”
This one arrives as a 1101. The runtime saw that all the code for the request had finished and nothing was left in the event loop, yet no Response came back. Other runtimes would hang forever. Workers throws so you can find the bug.
The usual cause is a Promise that never resolves or rejects:
export default {
fetch(req) {
const response = new Response("ok");
const { promise, resolve } = Promise.withResolvers();
// setTimeout(resolve, 0) is the missing line
return promise.then(() => response);
},
};
Find the Promise your Response depends on, in your code or a dependency, and make sure every path resolves or rejects it. The no-floating-promises ESLint rule catches Promises that are created and never handled.
The second cause is a WebSocket whose server side is never closed. On compatibility dates on or after 2026-04-07, the web_socket_auto_reply_to_close flag completes the close handshake for you, so this mostly bites Workers on older dates. On those, call server.close() in the close listener.
“Illegal invocation: function called with incorrect this reference”
A runtime method lost its this. The classic case is destructuring ctx:
export default {
async fetch(request, env, ctx) {
// Throws: waitUntil has lost its this
const { waitUntil } = ctx;
waitUntil(somePromise);
// Works: call it on ctx, or bind it first
ctx.waitUntil(somePromise);
const bound = ctx.waitUntil.bind(ctx);
bound(somePromise);
return fetch(request);
},
};
“Cannot perform I/O on behalf of a different request”
Each invocation has its own execution context. I/O objects such as streams, request bodies and responses belong to the request that created them. If you cache a Response in global scope and return it from a later request, the runtime throws this error. Cache the data and build a fresh Response each time:
let cachedData = null;
export default {
async fetch(request, env, ctx) {
if (cachedData) {
return new Response(cachedData);
}
const response = new Response("Hello, world!");
cachedData = await response.text();
return new Response(cachedData, response);
},
};
For state shared across requests, use Durable Objects. For cached data, use Workers KV.
Error 1019: the loop limit
A chain of Workers cannot call itself or another Worker more than 16 times. The CF-EW-Via header counts the invocations left and drops by one on each hop. When it reaches zero you get 1019. Look for a route or service binding that sends a request back into the same Worker.
When a limit stops the Worker: error 1102
Error 1102 shows the message Worker exceeded resource limits. The invocation outcome in analytics and Logpush tells you which limit it was: exceededCpu or exceededMemory.
Running out of CPU time
CPU time only counts time spent running your code. Waiting on fetch, KV reads or database queries does not count. Free Workers get 10 ms per request. Paid Workers default to 30 seconds and can go up to 5 minutes:
{
"limits": {
"cpu_ms": 300000
}
}
Before you raise the limit, profile CPU in DevTools locally to find the hot path. Then consider splitting heavy work across smaller requests or moving it into Durable Objects.
Running out of memory
Each isolate gets 128 MB, covering the JavaScript heap and WebAssembly allocations. One isolate serves many requests at once, so they all share that budget. When the isolate goes over, in-flight requests finish and the runtime starts a new isolate for the next ones. Buffering a large body can also throw Memory limit would be exceeded before EOF.
Work through the fixes in this order:
- Stream request and response bodies with
TransformStream. - Keep large data in KV, R2 or D1, out of Worker memory.
- If you use Zod, upgrade to 4.5.0 or later, since earlier versions use far more memory per schema.
- Take heap snapshots in DevTools locally to find leaks.
Mistakes that hide Workers errors
- Logging a stack from inside the Worker. Source maps are not available at runtime, so
console.log(err.stack)prints the minified trace. Let the exception reach the runtime, then read the remapped trace in real-time logs or Tail Workers. - Sending logs without
ctx.waitUntil(). Afetchto an external log service after the response has gone may never complete. Wrap it inctx.waitUntil(), which keeps work alive for up to 30 seconds after the response. - Sampling away the evidence. A
head_sampling_rateof 0.01 logs one request in a hundred, and a rare error may never show up. Keep the rate at 1 while you debug, and set a different rate per environment underenv.staging.observabilitywhen you need to. Also note that an account over 5 billion logs in a day drops to a 1% sample for the rest of that day. - Trusting
wrangler tailunder heavy traffic. Real-time logs can enter sampling mode and drop messages, with a warning in the stream. Filter what you watch, and remember that at most 10 clients, dashboard sessions andwrangler tailcalls combined, can watch one Worker at a time. Real-time logs store nothing, so use Workers Logs for history. - Waiting on WebSocket logs. In
wrangler tail,console.logcalls inside WebSocket handlers stay hidden until the client closes the connection, then flush all at once. - Blaming your code for a runtime problem. An unfamiliar
11xxcode often means the Workers runtime itself is struggling. Check the Cloudflare status page first.
Letting Polylane watch your Workers for errors
If you connect a Cloudflare account with an API token, Polylane can query its logs and metrics, triage its alerts into issues, and hand each confirmed issue to one agent that traces the cause and opens the fix as a pull request for your review. It also flags a Worker with logs disabled as an observability advisory that it can fix in place, which covers the first step on this page.
Running on Cloudflare? See how Polylane monitors Cloudflare in production.
Common questions.
What does Cloudflare Workers error 1101 mean?
Error 1101 means your Worker threw a JavaScript exception before it returned a response. Find the stack trace in Workers Logs or with `npx wrangler tail`. A 1101 that says `The script will never generate a response` usually means a Promise that never resolves, or a WebSocket that is never closed on older compatibility dates.
What causes error 1102 and how do I fix it?
Error 1102 means the Worker exceeded its CPU time or its 128 MB memory limit. Check **Metrics** > **Errors** > **Invocation Statuses** to see which one. For CPU on the Paid plan, raise `cpu_ms` from the 30 second default up to 300000 (5 minutes). For memory, stream bodies and keep large data in KV, R2 or D1.
Why does my Workers stack trace point at minified code?
Source maps are not uploaded. Add `"upload_source_maps": true` to your Wrangler config and redeploy with Wrangler 3.46.0 or later, and keep each map under 15 MB gzipped. Remapped traces appear in real-time logs and Tail Workers, but a `console.log(err.stack)` inside the Worker stays minified.
How long are Workers Logs kept?
Workers Logs keeps data for 3 days on the Free plan and 7 days on the Paid plan. The Free plan writes 200,000 log events a day. The Paid plan includes 20 million a month and charges $0.60 per additional million. For longer retention, export with OpenTelemetry, Workers Logpush or Tail Workers.
Can I set breakpoints in a Cloudflare Worker?
Yes, in local development. Run `wrangler dev` and press `D` to open DevTools, or attach VS Code or WebStorm to port 9229 with a launch configuration. For a deployed Worker, use Workers Logs, `wrangler tail` and source-mapped stack traces, then replay the failing request locally.
Why is wrangler tail missing some of my logs?
Under heavy traffic, real-time logs can enter sampling mode and drop messages, with a warning in the stream. Filter the stream to cut volume, and note that only 10 clients can watch one Worker at a time. Logs inside WebSocket handlers also stay hidden until the client closes the connection.
How do I see which binding call failed in a local request?
Current versions of `wrangler dev` and `vite dev` record OpenTelemetry traces for every local invocation automatically. Press `e` in Wrangler or open `/cdn-cgi/explorer` to view spans for fetch calls and for KV, R2, D1, Durable Objects and Queues bindings. Coding agents can query the same traces through the Local Explorer API.
Sources
- Errors and exceptions · Cloudflare Workers docs
- Source maps and stack traces · Cloudflare Workers docs
- Workers Logs · Cloudflare Workers docs
- Real-time logs · Cloudflare Workers docs
- Limits · Cloudflare Workers docs
- DevTools · Cloudflare Workers docs
- Wrangler commands for Workers
- Debugging logs example · Cloudflare Workers docs
- Better debugging for Cloudflare Workers, now with breakpoints
- Your agent can now debug Workers with local tracing
- Polylane documentation
- Polylane full content
Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.
More in this series
- 1 How to Monitor a Django App on Render
- 2 How to Monitor a FastAPI App on Railway: Logs, Traces and Alerts
- 3 How to Monitor a Supabase App in Production
- 4 How to Debug Cloudflare Workers Errors: Logs, Traces and Error Codes
- 5 How to debug Vercel function timeouts
- 6 Vercel 504 Gateway Timeout on Serverless Functions: Causes and Fixes
- 7 Cloudflare Workers error 1101: causes and how to fix it
- 8 Cloudflare Hyperdrive connection errors: causes and fixes
- 9 How to Monitor a Convex App in Production
Related
- What Is an AI SRE? How It Works and How to Evaluate One
An AI SRE is an AI agent that triages alerts, investigates incidents and proposes fixes. Learn how it works, the autonomy levels and how to evaluate one.
- Fix Cloudflare “Error 1102: Worker exceeded resource limits”
Why a Cloudflare Worker returns Error 1102, how to tell a CPU time overrun from a 128 MB memory overrun, and the code and Wrangler changes that fix each.
- Fix “Durable Object reset because its code was updated”
Why a deploy makes Cloudflare Durable Objects throw “reset because its code was updated”, and how to retry safely, keep clients connected and lose no state.
- Workers Issues: route Cloudflare errors to coding agents
Cloudflare's new Workers Issues groups exceptions and 5xx errors and sends them to Claude Code, Cursor, Devin or a webhook. Setup, automations and limits.
- How We Fixed Our Cloudflare Durable Objects Memory Exceeded Errors
Cloudflare Durable Objects run in V8 isolates with a hard 128 MB limit. Ours idled at ~140 MB and was reset ~300 times a day. 130 MB of zod schemas were built at module load, most of them by code that was never called. How we profiled the production bundle, the two fixes, and the numbers, 218 MB to 82 MB and resets to zero.