Enable Workers Logs and Traces for production
Production Workers without observability are a black box. Enable logs and traces before you deploy to production. When an intermittent error appears, you need data already being collected to diagnose it. Enable them in your Wrangler configuration using the observability section with head_sampling_rate to control volume and manage costs. A sampling rate of 1 captures everything; lower it for high-traffic Workers. Use structured JSON logging with console.log so logs are searchable and filterable. Use console.error for errors and console.warn for warnings; these appear at the correct severity level in the Workers Observability dashboard.
Example: Structured JSON logging
This example shows how to use structured JSON logging with console.log so logs are searchable and filterable in the Workers Observability dashboard:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
try {
console.log(
JSON.stringify({
message: "incoming request",
method: request.method,
path: url.pathname,
}),
);
const result = await env.MY_KV.get(url.pathname);
return new Response(result ?? "Not found", {
status: result ? 200 : 404,
});
} catch (e) {
console.error(
JSON.stringify({
message: "request failed",
error: e instanceof Error ? e.message : String(e),
path: url.pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
Deploy Hook build tracking in dashboard
After triggering a Deploy Hook, you can verify it from the dashboard: the Deploy Hooks list shows when each hook was last triggered, and in the Worker's build history, the Triggered by column identifies builds started by a Deploy Hook using the hook name and a deploy hook label. Hook-triggered builds are recorded with build_trigger_source set to deploy_hook.
Pull request comment history in GitHub
Comment history reveals any builds completed earlier while the pull request was open.
GitHub check runs for monorepo builds
If you have one or multiple Workers connected to a repository (such as a monorepo), you can check on the status of each build within GitHub via GitHub check runs. Check runs will appear with details showing the build ID and project (Script) associated with each check.
Pull request comment with preview URL
When a commit is on a pull request, Cloudflare automatically posts a comment on the pull request with the build status. A preview URL is provided for any builds which perform wrangler versions upload, allowing comparison of code changes alongside an updated version of the Worker.
View build status and logs in Deployments tab
Monitor a build's status and view build logs by navigating to View build history at the bottom of the Deployments tab. For successful builds, select View build to see build details in the associated new version under Version History, which also displays the preview URL.
Python example: debugging logs with error responses
from workers import WorkerEntrypoint
from pyodide.ffi import create_proxy
from js import Response, fetch
async def post_log(data):
log_url = "https://log-service.example.com/"
await fetch(log_url, method="POST", body=data)
class Default(WorkerEntrypoint):
async def fetch(self, request):
response = await fetch(request)
try:
if not response.ok and not response.redirected:
body = await response.text()
raise Exception(f'Bad response at origin. Status:{response.status} Body:{body.strip()[:10]}')
except Exception as e:
self.ctx.waitUntil(create_proxy(post_log(str(e))))
response = Response.new(stack, response)
response.headers["X-Debug-err"] = str(e)
return response
ctx.waitUntil() required for logging service completion
Without ctx.waitUntil(), a fetch() to an external logging service may or may not complete before the Workers runtime terminates the execution context. Wrapping asynchronous operations like sending logs in ctx.waitUntil() ensures they finish executing.
TypeScript example: debugging logs with error responses
interface Env {}
export default {
async fetch(request, env, ctx): Promise<Response> {
const LOG_URL = "https://log-service.example.com/";
async function postLog(data) {
return await fetch(LOG_URL, {
method: "POST",
body: data,
});
}
let response;
try {
response = await fetch(request);
if (!response.ok && !response.redirected) {
const body = await response.text();
throw new Error(
"Bad response at origin. Status: " +
response.status +
" Body: " +
body.trim().substring(0, 10),
);
}
} catch (err) {
ctx.waitUntil(postLog(err.toString()));
const stack = JSON.stringify(err.stack) || err;
response = new Response(stack, response);
response.headers.set("X-Debug-stack", stack);
response.headers.set("X-Debug-err", err);
}
return response;
},
} satisfies ExportedHandler<Env>;
Hono example: debugging logs with middleware error handling
import { Hono } from 'hono';
interface Env {}
const app = new Hono<{ Bindings: Env }>();
const LOG_URL = "https://log-service.example.com/";
async function postLog(data: string) {
return await fetch(LOG_URL, {
method: "POST",
body: data,
});
}
app.use('*', async (c, next) => {
try {
await next();
if (c.res && (!c.res.ok && !c.res.redirected)) {
const body = await c.res.clone().text();
throw new Error(
"Bad response at origin. Status: " +
c.res.status +
" Body: " +
body.trim().substring(0, 10)
);
}
} catch (err) {
c.executionCtx.waitUntil(
postLog(err.toString())
);
const stack = JSON.stringify(err.stack) || err.toString();
const response = c.res ?
new Response(stack, {
status: c.res.status,
headers: c.res.headers
}) :
new Response(stack, { status: 500 });
response.headers.set("X-Debug-stack", stack);
response.headers.set("X-Debug-err", err.toString());
c.res = response;
}
});
app.all('*', async (c) => {
return fetch(c.req.raw);
});
export default app;
Debug headers for error responses
When sending error responses, include debug information in custom headers. Common headers include X-Debug-stack for the error stack trace and X-Debug-err for the error message itself, allowing clients and monitoring systems to inspect error details.
JavaScript example: debugging logs with error responses
export default {
async fetch(request, env, ctx) {
const LOG_URL = "https://log-service.example.com/";
async function postLog(data) {
return await fetch(LOG_URL, {
method: "POST",
body: data,
});
}
let response;
try {
response = await fetch(request);
if (!response.ok && !response.redirected) {
const body = await response.text();
throw new Error(
"Bad response at origin. Status: " +
response.status +
" Body: " +
body.trim().substring(0, 10),
);
}
} catch (err) {
ctx.waitUntil(postLog(err.toString()));
const stack = JSON.stringify(err.stack) || err;
response = new Response(stack, response);
response.headers.set("X-Debug-stack", stack);
response.headers.set("X-Debug-err", err);
}
return response;
},
};
MCP server for Workers observability
The cloudflare-observability MCP server at https://observability.mcp.cloudflare.com/mcp can be connected to AI agents to check logs, look for exceptions, and automatically fix issues in Workers applications.
Workers observability capabilities
Cloudflare Workers provides built-in observability with real-time logs and analytics for monitoring performance, debugging issues, and analyzing traffic.
Logging in Python Worker
Python Workers support three logging methods: (1) JavaScript console APIs via `from js import console`, accessible as console.log(), console.error(), etc.; (2) native Python logging via the logging module with configurable levels (default is warning); (3) built-in print() function. All three methods are available in fetch() and scheduled() methods.
Local Explorer observability: automatic tracing and logging
Local Explorer automatically captures traces and logs from every Worker invocation during wrangler dev without code modification. The captured data includes the same instrumentation as production Workers Logs and Traces: invocation logs, binding operations, timing, and console output, all available in the browser during development.
Local Explorer logs view
The Logs view in Local Explorer captures all console.* output from your Worker. You can filter by log level (error, warn, info, log, debug) or search by text to find specific messages.
Local Explorer traces view
Each Worker invocation appears as a trace in Local Explorer. Selecting a trace shows every binding operation with timing, status, and error details. Tracing captures operations made through remote bindings, allowing inspection of calls from a locally running Worker to deployed resources.
Breakpoint debugging with VSCode JavaScript Debug Terminal
You can debug a Worker locally using VSCode's JavaScript Debug Terminal without any configuration. Open a JS debug terminal by pressing Cmd + Shift + P and typing 'javascript debug', then run 'wrangler dev' or 'vite dev' from within the debug terminal. VSCode will automatically connect to your running Worker and start a debugging session, even if you're running multiple Workers at once.
Breakpoints available in local and deployed Workers
You can debug your local Workers using Wrangler or Vite with breakpoints, as well as deployed Workers. Breakpoints provide the ability to review what is happening at a given point in the execution of your Worker.
Chrome DevTools breakpoint debugging for Workers
Breakpoint functionality for Workers is available in both Chrome DevTools and VS Code. For more information on breakpoint debugging via Chrome's DevTools, refer to Chrome's article on breakpoints.
VS Code launch.json applies to single workspace only
The .vscode/launch.json file only applies to a single workspace. To have the launch configuration available for all your workspaces, add it to your User Settings instead, per the official VS Code documentation on global launch configuration.
Minification incompatible with --remote breakpoint debugging
When debugging using the --remote flag, code minification must not be enabled. Do not set minify to true in your Wrangler configuration file because the debugger will be unable to find variables when stopped at a breakpoint.
Breakpoint debugging with --remote flag cost warning
Using breakpoint debugging in wrangler dev with the --remote flag can extend Worker CPU time and incur additional costs because you are testing against actual resources that count against usage limits. It is recommended to use wrangler dev without the --remote option to develop locally instead.
VS Code launch.json configuration for Wrangler breakpoint debugging
To set up breakpoint debugging in VS Code using a launch.json file, create a .vscode/launch.json file in your project root with a configuration object containing: name: 'Wrangler', type: 'node', request: 'attach', port: 9229, cwd: '/', resolveSourceMapLocations: null, attachExistingChildren: false, autoAttachChildProcesses: false, and sourceMaps: true (the sourceMaps line is optional). After creating this file, run 'npx wrangler dev', select the Wrangler configuration in the Run & Debug panel, and add breakpoints in your .js or .ts files. When you visit the Worker's local URL (default http://127.0.0.1:8787), the breakpoint will be hit.
CPU profile visualization modes in DevTools
The CPU profile chart view shows a timeline at the top and a breakdown of CPU time used for operations below, with fetch time at the top and subscomponents nested beneath. An alternative "Heavy (Bottom Up)" view shows the relative times allocated to each function, making it easier to identify the slowest portions of the Worker.
Timers only increment on I/O in production Workers
Workers only increment timers on I/O for security purposes, which makes measuring CPU execution times difficult in production. However, measuring CPU execution times is possible in local development with DevTools.
CPU profiling in DevTools workflow
To generate a CPU profile in DevTools: Run `wrangler dev` to start your Worker, press the `D` key from your terminal to open DevTools, select the "Profiler" tab, select `Start` to begin recording CPU usage, send requests to your Worker from a new tab, then select `Stop` to complete the profile.
CPU profile example Worker showing addNumbers and moreAddition functions
Example Worker code demonstrating CPU profiling:
```js
const addNumbers = (body) => {
for (let i = 0; i < 5000; ++i) {
body = body + " " + i;
}
return body;
};
const moreAddition = (body) => {
for (let i = 5001; i < 15000; ++i) {
body = body + " " + i;
}
return body;
};
export default {
async fetch(request, env, ctx) {
let body = "Hello Profiler! - ";
body = addNumbers(body);
body = moreAddition(body);
return new Response(body);
},
};
```
This example shows how to identify CPU-heavy code using DevTools profiling. The `addNumbers` function uses 0.3ms of CPU time while `moreAddition` uses 2.2ms, allowing developers to optimize the slower function.
Cloudflare's Chrome DevTools implementation
Cloudflare provides a custom implementation of Chrome DevTools specifically for Workers, available when using Wrangler CLI or Vite with the Cloudflare Vite plugin for local development.
Chrome DevTools capabilities for Workers
Chrome DevTools for Cloudflare Workers supports viewing logs directly in the Chrome console, debugging code by setting breakpoints, profiling CPU usage, and observing memory usage to debug memory leaks that can cause out-of-memory (OOM) errors.
Replicating production behavior for memory profiling
When using DevTools to profile memory, it may be difficult to replicate specific behavior you are seeing in production. To mimic production behavior, make sure the requests you send to the local Worker are similar to requests in production, which might mean sending a large volume of requests, making requests to specific routes, or using production-like data with the --remote flag.
Memory leak example: appending to global variable
A common memory leak pattern in Workers occurs when appending to a global variable inside the fetch handler without clearing it. For example, executing 'responseText = responseText + ` (Requested at: ${now})`' repeatedly causes the global variable to grow with each request, consuming increasingly large amounts of memory.
Taking a memory snapshot in DevTools
To generate a memory snapshot: run 'wrangler dev' to start your Worker, press the 'D' key in your terminal to open DevTools, select the Memory tab, send requests to your Worker to start allocating memory (optionally include a debugger to pause execution at the proper time), and select 'Take snapshot' to inspect Worker memory.
Memory Summary view in DevTools
The Memory Summary lists data types by the amount of memory they take up. Clicking into a specific data type (such as '(string)') shows individual objects and their memory consumption, allowing you to identify large objects and pinpoint the source of memory leaks.
Memory Statistics view in DevTools
The Statistics view in the Memory tab dropdown provides a general sense of what takes up memory. It breaks down memory usage by data type and shows the amount of memory allocated to each type, helping identify potential sources of memory leaks.
Memory profiling with DevTools snapshots
You can profile memory usage in Workers using snapshots in DevTools. Memory snapshots let you view a summary of memory usage, see how much memory is allocated to different data types, and get details on specific objects in memory.
Axiom OTLP endpoints for traces and logs
Axiom provides separate OTLP endpoints for sending telemetry data. Traces are sent to https://api.axiom.co/v1/traces and logs are sent to https://api.axiom.co/v1/logs.
Configuring Axiom destination headers in Cloudflare dashboard
When creating an Axiom destination in the Cloudflare Workers Observability section, two custom headers are required: an Authorization header with value Bearer <your-api-token>, and an X-Axiom-Dataset header with the value of your dataset name.
Delay in Axiom data appearance after Worker deployment
After deploying a Worker with Axiom observability configuration, it may take a few minutes for data to appear in the Axiom dashboard.
Axiom dataset prerequisites for Cloudflare Workers
Before exporting telemetry to Axiom, you must have an active Axiom account (free tier available), a deployed Cloudflare Worker that you want to monitor, and an Axiom dataset created to receive the telemetry data.
Axiom API token format and permissions
Axiom API tokens follow the format xaat-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. When creating a token for Cloudflare Workers telemetry export, you must select the Ingest permission to allow sending telemetry data. The token must be copied immediately after creation as it cannot be viewed again.
Grafana Cloud telemetry data appearance delay
After deploying a Worker with Grafana Cloud observability configured, it may take a few minutes after deployment for telemetry data to appear in Grafana Cloud.
Grafana Cloud observability integration overview
Grafana Cloud is a fully managed observability platform. By exporting Cloudflare Workers telemetry to Grafana Cloud, you can visualize distributed traces in Grafana Tempo to understand request flows and performance bottlenecks, and query and analyze logs in Grafana Loki alongside traces.
Grafana Cloud destination configuration
To set up a destination in Cloudflare: 1) Navigate to your Cloudflare account's Workers Observability section at https://dash.cloudflare.com/?to=/:account/workers-and-pages/observability/pipelines. 2) Click Add destination and configure a destination name (e.g., grafana-tracing). 3) From Grafana, copy your OTEL endpoint, auth header, and auth value. The OTEL endpoint will look like https://otlp-gateway-prod-us-east-2.grafana.net/otlp (append /v1/traces for traces and /v1/logs for logs). The custom header should include Authorization as the header name and Basic MTMxxx... as the header value.
Grafana Cloud OpenTelemetry setup steps
To set up OpenTelemetry export to Grafana Cloud: 1) Log in to your Grafana Cloud portal. 2) Navigate to Connections → Add new connection. 3) Search for and select OpenTelemetry (OTLP). 4) Select Quickstart then select JavaScript. 5) Click Create a new token. 6) Enter a token name (e.g., cloudflare-workers-otel) and click create token. 7) Click Close without copying the token. 8) Copy and save the OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS values from the Environment variables code block as the OTel endpoint and Auth header value respectively.
Prerequisites for Grafana Cloud integration
To export OpenTelemetry data to Grafana Cloud, you need an active Grafana Cloud account (free tier available) and a deployed Worker that you want to monitor.
Honeycomb OTLP endpoints for traces and logs
Honeycomb provides separate OTLP endpoints for different telemetry data types: Traces endpoint is https://api.honeycomb.io/v1/traces and Logs endpoint is https://api.honeycomb.io/v1/logs.
Telemetry data propagation delay to Honeycomb
After deploying a Worker with Honeycomb telemetry export enabled, it may take a few minutes for data to appear in Honeycomb.
Honeycomb Ingest API Key permissions
When creating a Honeycomb Ingest API Key for OTLP ingestion, the "Can create services/datasets" permission must be selected as it is required for OTLP ingestion functionality.
Honeycomb API key format and retrieval
Honeycomb API keys start with the prefix hcaik_ followed by alphanumeric characters (e.g., hcaik_01hq...). API keys must be copied immediately after creation and stored securely, as they cannot be viewed again after being generated.
Honeycomb destination configuration for logs
To configure a logs destination in Cloudflare Workers Observability, set the Destination Name to a descriptive value like honeycomb-logs, select Logs as the Destination Type, enter https://api.honeycomb.io/v1/logs as the OTLP Endpoint, and add a Custom Header with name x-honeycomb-team and the Honeycomb API key as the value.
Honeycomb capabilities for Workers observability
Honeycomb allows Cloudflare Workers users to visualize traces to understand request flows and identify performance bottlenecks, query and analyze logs with unlimited dimensionality across any attribute, and create custom queries and dashboards to monitor Workers.
Cannot perform I/O on behalf of a different request error
The error 'Cannot perform I/O on behalf of a different request' occurs when you attempt to share input/output (I/O) objects (such as streams, requests, or responses) created by one invocation of your Worker in the context of a different invocation. Each invocation is handled independently and has its own execution context. This error is most commonly caused by attempting to cache an I/O object, like a Request in global scope, and then access it in a subsequent request. Fix this by storing only the data in global scope, rather than the I/O object itself. If you need to share state across requests, consider using Durable Objects. If you need to cache data across requests, consider using Workers KV.
Illegal invocation error: function called with incorrect this reference
The error 'TypeError: Illegal invocation: function called with incorrect this reference' occurs when calling a function that relies on 'this', but the value of 'this' has been lost. This is typically caused by destructuring runtime-provided JavaScript objects that have functions relying on 'this', such as 'ctx'. To avoid this, directly call the method on the original object (e.g., ctx.waitUntil(somePromise)) or re-bind the function to the original context using apply, call, or bind.
Script will never generate a response error with unclosed WebSocket connections
If a WebSocket is missing the proper code to close its server-side connection, the Workers runtime will throw a 'script will never generate a response' error. Ensure that the WebSocket's server-side connection is properly closed via an event listener or other server-side logic. With the web_socket_auto_reply_to_close compatibility flag (enabled by default on compatibility dates on or after 2026-04-07), the runtime automatically completes the WebSocket close handshake, making this error scenario less likely to occur.
Script will never generate a response error with unresolved Promises
The error 'The script will never generate a response' occurs when the Workers runtime detects that all the code associated with the request has executed and no events are left in the event loop, but a Response has not been returned. One cause is relying on a Promise that is never resolved or rejected, which is required to return a Response. To debug, look for Promises within your code or dependencies' code that block a Response, and ensure they are resolved or rejected. The no-floating-promises eslint rule can help prevent this by reporting when a Promise is created and not properly handled.
Worker error codes when preventing response
When a Worker running in production has an error that prevents it from returning a response, the client receives an error page. Error code 1101: Worker threw a JavaScript exception. Error code 1102: Worker exceeded CPU time limit. Error code 1103: The owner of this worker needs to contact Cloudflare Support. Error code 1019: Worker hit loop limit. Error code 1021: Worker has requested a host it cannot access. Error code 1022: Cloudflare has failed to route the request to the Worker. Error code 1024: Worker cannot make a subrequest to a Cloudflare-owned IP address. Error code 1027: Worker exceeded free tier daily request limit. Error code 1042: Worker tried to fetch from another Worker on the same zone, which is only supported when the global_fetch_strictly_public compatibility flag is used. Error code 10162: Module has an unsupported Content-Type. Other 11xx errors generally indicate a problem with the Workers runtime itself.
Example: passThroughOnException() in Module Worker
export default {
async fetch(request, env, ctx) {
ctx.passThroughOnException();
// an error here will return the origin response, as if the Worker wasn't present
return fetch(request);
},
};
This example shows how to use ctx.passThroughOnException() to forward requests to the origin if an exception is thrown during the Worker's execution.