Get started Dashboard
Observability ·
Part 12 of Platform playbooks

How to trace requests across Cloudflare Workers and service bindings

Explore with AI

Set observability.traces.enabled to true in the Wrangler config of every Worker in the chain, then redeploy each one. Since May 2026, Cloudflare passes trace context across service binding and Durable Object calls for you, so the called Worker's spans show up as children inside the caller's trace. You can read that trace in the Workers Observability dashboard or export it to any OTLP endpoint, and tracing to services outside Cloudflare is handled separately.

On this page

A request that enters one Worker and then calls two more through service bindings used to leave you with three traces that had nothing joining them. Since 7 May 2026, Workers automatic tracing passes trace context across Worker-to-Worker subrequests on its own. Service binding and Durable Object calls now show up as nested child spans in one trace. You get this by turning tracing on in every Worker the request touches. You don’t need an SDK, and you don’t need to copy headers by hand.

This page covers the setup, how to read the RPC spans that join one Worker to the next, how to add your own spans, how to export the joined trace over OpenTelemetry, and the places where a cross-Worker trace still breaks.

What a cross-Worker trace is

A trace is the full story of one request. Each operation in it is a span: a timed piece of work with a name, a start, a duration and some attributes. Child spans sit inside parent spans, so the trace reads as a tree. On Workers, the runtime creates these spans for you. It traces the handler, every outbound fetch, binding calls to KV, R2, D1, Queues and Durable Objects, and RPC calls between Workers.

A service binding lets one Worker call another directly, through env.AUTH.fetch(...) or an RPC method like env.AUTH.verify(token). To trace across it, the caller has to hand its trace context to the callee, and the callee’s spans have to attach under the caller’s span. Before the May 2026 change, automatic tracing didn’t do this. The caller’s trace stopped at the binding call, and the callee began a fresh trace of its own. Now the context is passed automatically, so a gateway Worker, the auth Worker it calls and the Durable Object that Worker calls all land in one trace view.

How a request becomes one trace across a gateway Worker, a service binding and a Durable Object

graph TB A[Request hits the gateway Worker] --> B[Root span for the fetch handler] B --> C[Service binding call to auth Worker] C --> D{Tracing on in the auth Worker?} D -->|no| E[Auth spans missing from the trace] D -->|yes| F[RPC spans nest under the caller] F --> G[Durable Object call nests below] G --> H[One trace in dashboard or OTLP tool] style E fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d style H fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Turning on tracing in every Worker in the chain

Tracing is set per Worker, so do this for each Worker a request can pass through.

  1. Open the Wrangler file of the first Worker (the one with the public route) and add a traces block under observability:
// wrangler.jsonc for gateway-worker
{
  "name": "gateway-worker",
  "observability": {
    "logs": { "enabled": true },
    "traces": {
      "enabled": true,
      "head_sampling_rate": 1
    }
  },
  "services": [
    { "binding": "AUTH", "service": "auth-worker" }
  ]
}
  1. Add the same traces block to every Worker behind a binding. The auth Worker in this example needs its own setting:
// wrangler.jsonc for auth-worker
{
  "name": "auth-worker",
  "observability": {
    "logs": { "enabled": true },
    "traces": { "enabled": true }
  }
}

The TOML form is the same thing:

[observability.traces]
enabled = true
head_sampling_rate = 1
  1. Don’t rely on observability.enabled = true alone. That setting turns on logs only. Cloudflare plans to have it turn on tracing as well in future, behind a newer compatibility date. While tracing is in beta, the explicit traces.enabled key is what records traces.

  2. Redeploy every Worker you changed:

npx wrangler deploy
  1. Send a request through the chain, then open the trace for the first Worker in the Workers Observability dashboard. The root span is the gateway’s fetch handler. The auth Worker’s work should sit inside it as child spans. If the root span shows Trace in Progress, give it a few moments to finish.

RPC across a service binding looks like this in code. The runtime traces each method call, and you don’t add anything:

// auth-worker/src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";

export class AuthService extends WorkerEntrypoint {
  async verify(token: string) {
    const session = await this.env.SESSIONS.get(token, "json");
    return session ?? null;
  }
}

// gateway-worker/src/index.ts
export default {
  async fetch(request: Request, env: Env) {
    const token = request.headers.get("authorization") ?? "";
    const user = await env.AUTH.verify(token); // traced as an RPC call
    if (!user) return new Response("Unauthorised", { status: 401 });
    return env.ORDERS.fetch(request); // traced as a binding fetch
  },
};

I built and led the Workers observability team at Cloudflare. When a cross-Worker trace looks broken, the first thing I check is whether a Worker in the middle of the chain was deployed without tracing turned on.

Reading the RPC spans that join one Worker to the next

Cloudflare emits three kinds of span for RPC between Workers, and between Workers and Durable Objects. Knowing which side made each span is how you tell caller latency apart from callee latency.

  • RPC session: a span on the caller’s side that covers the whole RPC session. Calls that reuse the session appear under it.
  • RPC invocation: a span on the callee’s side that covers the invocation inside the target Worker or Durable Object.
  • RPC call: a span for one method call or property access, on either side.

The RPC call spans carry these attributes:

  • jsrpc.method: the method name or property path, such as verify
  • jsrpc.operation: call or getProperty
  • jsrpc.target_kind: the type of RPC target
  • jsrpc.caller_span_id: on spans from the callee’s side, the ID of the matching jsRpcCall span on the caller’s side

The dashboard draws caller-side and callee-side spans in the colour of the Worker they run in, so the colour change marks where one Worker hands off to the next. When a binding call is slow, compare the caller’s RPC call span with the callee’s invocation span. If the callee’s handler is short but the caller waits a long time, the time is going somewhere between the two. If the callee’s handler is long, open its child spans: a D1 query, a KV read or an outbound fetch will usually account for it.

A few attributes on every span help when you’re grouping across Workers:

  • cloudflare.script_name and faas.name give the Worker’s name.
  • cloudflare.script_version.id gives the deployed version, which helps when one Worker in the chain was redeployed just before the trouble began.
  • cloudflare.durable_object.id appears on Durable Object spans.
  • Root spans carry cloudflare.outcome (ok, exception, exceededCpu, exceededMemory), cloudflare.cpu_time_ms and cloudflare.wall_time_ms.

For filtering by Worker across both traces and logs, Cloudflare recommends $metadata.service. Attributes like service.name only appear on some spans.

Adding custom spans so the trace explains your own code

Automatic spans show I/O. They don’t show which part of your own logic ran. Custom spans fill that gap, and they nest under the automatic spans with no extra wiring. You can use the API in two ways: import tracing from cloudflare:workers (this works anywhere, including library code), or use ctx.tracing inside a handler.

import { tracing } from "cloudflare:workers";

export default {
  async fetch(request: Request, env: Env) {
    return tracing.enterSpan("checkout", async (span) => {
      span.setAttribute("url.path", new URL(request.url).pathname);

      // The RPC call to the auth Worker nests under "checkout"
      const user = await env.AUTH.verify(request.headers.get("authorization") ?? "");
      span.setAttribute("user.plan", user?.plan ?? "anonymous");

      return env.ORDERS.fetch(request);
    });
  },
};

Pick the method by the lifetime of the work:

  • enterSpan() ends the span when the callback returns or its promise settles. Use it for most code.
  • startActiveSpan() keeps the span open after the callback returns, until you call span.end(). Use it for streams.
  • startSpan() creates a span that never becomes active, so nothing nests under it. You end it yourself.

tracing.getActiveSpan() lets you add attributes such as user.id to the current span, or to the root span, without passing the span object around. span.recordException(err) attaches an exception event. Any console.log inside a span is linked to that span, so your logs line up with the trace.

Two data limits apply. Span names are cut at 64 bytes, and each span holds roughly 64 KB of attribute and exception data. Once a span hits that limit, further attributes are dropped and cloudflare.warning.type is set to span_data_limit_exceeded.

Sending the joined trace to Honeycomb, Grafana Cloud, Axiom or Datadog

Workers traces follow OpenTelemetry, so the joined trace can go to any tool with an OTLP endpoint. Cloudflare lists endpoints for Honeycomb, Grafana Cloud, Axiom, Sentry, Datadog, New Relic, Splunk, SigNoz and others.

  1. In the Cloudflare dashboard, go to Observability > Destinations and select Add destination.
  2. Give it a name such as honeycomb-traces, set the type to Traces, and enter the provider’s endpoint, for example https://api.honeycomb.io/v1/traces. Add the auth headers your provider asks for. Cloudflare doesn’t accept the binary protobuf form of OTLP.
  3. Point every Worker in the chain at the destination:
{
  "observability": {
    "traces": {
      "enabled": true,
      "destinations": ["honeycomb-traces"],
      "head_sampling_rate": 0.05,
      "persist": false
    }
  }
}
  1. Redeploy. Events can take several minutes to reach the destination. Back in Destinations, the status reads Never run if nothing has been sent yet, and Error if the endpoint or headers are wrong.

persist defaults to true, which exports traces and also stores them in Cloudflare. Workers traces are kept for seven days. From 1 December 2026, stored traces count towards account-level ingestion and storage under Cloudflare Observability pricing. Set persist: false if your external tool is the only place you read traces.

Sampling a chain of Workers without losing the trace you need

The default head_sampling_rate is 1, which traces every request. The valid range is 0 to 1, and 0.05 traces five requests in every hundred. Sampling is head-based, so requests that aren’t traced carry no tracing overhead. You can set a separate rate for logs and for traces.

On busy Workers, lower the rate to control volume and cost. Keep in mind that a rare failure in a downstream Worker only shows up if the request that hit it was sampled. While you’re debugging a specific cross-Worker problem, raise the rate to 1 on the Workers involved, then lower it again afterwards. Inside your code, span.isTraced tells you whether the current request is being sampled, so you can skip building expensive attributes when it isn’t.

Tracing past the edge of Cloudflare

Automatic propagation covers Worker-to-Worker and Worker-to-Durable-Object calls. The Workers tracing docs still list one gap: when you export traces, trace IDs aren’t passed to services outside Cloudflare. A trace from your Worker won’t join a trace from an API running on another cloud. Cloudflare says it is working on propagation using the W3C Trace Context standard.

Cloudflare Traces, which went into open beta on 2 October 2026, works at the domain level. It traces security rules, transforms, cache, routing, Worker execution and the origin. It accepts and forwards W3C traceparent headers, so the trace can carry on into your origin’s tracing. You turn it on per domain in the dashboard, set a baseline sampling rate, and use Trace Rules to trace 100% of the traffic for one hostname, source IP or request header while the rest stays at the baseline rate.

To test locally, wrangler dev captures a trace for every local Worker invocation, including binding operations. Open /cdn-cgi/local/explorer on your local development server to see them.

Where cross-Worker traces still go wrong

  • One Worker in the chain has tracing off. That Worker’s spans won’t appear under the caller. Check the Wrangler file of every bound Worker, then redeploy it.
  • Only observability.enabled is set. For now that turns on logs only. Add observability.traces.enabled = true.
  • Spans that show 0 ms. Workers only update the clock when I/O happens, as a guard against Spectre attacks. CPU-only work can show as 0 ms even when it took longer. Check cloudflare.cpu_time_ms on the root span for the real CPU cost.
  • span_not_ended warnings. A span created with startActiveSpan or startSpan was still open when the invocation ended. The runtime closes it for you and adds the warning. Call span.end() in a finally block.
  • Grouping by service.name. Some spans don’t have it. Group by $metadata.service.
  • Span and attribute names that change. Tracing is in beta, and Cloudflare may rename spans and attributes to match OpenTelemetry conventions. Saved queries and dashboards in your OTLP tool may need updating when that happens.
  • A binding that loops back into the same Worker. A Worker chain can’t call itself or another Worker more than 16 times. Past that, the request fails with error 1019. A trace that repeats the same Worker over and over usually means a route or binding is pointing back at the Worker that made the call.
  • No manual trace IDs yet. The custom spans API doesn’t expose spanContext() yet, so you can’t read the trace ID and pass it on yourself, for example through a queue message.

Polylane on Cloudflare Workers in production

Polylane detects production issues and fixes them by opening pull requests. It connects to Cloudflare, so when a request starts failing somewhere in your chain of Workers, it works out why, and the fix arrives ready for your team’s normal review.

Running on Cloudflare? See how Polylane monitors Cloudflare in production.

Common questions.

Do I need an SDK to trace across Cloudflare service bindings?

No. Since 7 May 2026, Workers automatic tracing passes trace context across service binding and Durable Object calls. Set observability.traces.enabled to true in every Worker in the chain, then redeploy each one.

Why does my called Worker not show up in the caller's trace?

Usually the called Worker doesn't have tracing turned on, or hasn't been redeployed since you turned it on. Setting observability.enabled = true alone turns on logs only while tracing is in beta. Each Worker needs its own traces.enabled setting.

How do I tell caller latency from callee latency in a service binding call?

Compare the caller's RPC call span with the callee's RPC invocation span. Callee-side spans carry jsrpc.caller_span_id, which points to the matching caller span. If the callee's handler is short but the caller waits a long time, the time is being lost between the two Workers.

Can I send cross-Worker traces to Honeycomb, Grafana Cloud or Datadog?

Yes. Create a Traces destination under Observability > Destinations with the provider's OTLP endpoint and auth headers. Then list its name in observability.traces.destinations in each Worker's Wrangler config. Events can take several minutes to arrive.

Do Workers traces link to my backend running outside Cloudflare?

Not through Workers tracing alone. The docs say trace IDs aren't yet passed to services outside Cloudflare. Cloudflare Traces, in open beta since 2 October 2026, accepts and forwards W3C traceparent headers at the domain level, so the trace can continue into your origin.

How long are Workers traces kept and what do they cost?

Workers traces are kept for seven days. From 1 December 2026, stored traces count towards account-level ingestion and storage under Cloudflare Observability pricing. Lowering head_sampling_rate, or setting persist to false when you export, cuts the billable usage.

What sampling rate should I use across a chain of Workers?

The default is 1, which traces every request. On busy Workers a rate such as 0.05 traces five requests in every hundred. Raise it to 1 on the Workers involved while you debug a rare cross-Worker failure, because an unsampled request leaves no trace.

Sources (12)

  1. Automatic tracing across Durable Object and Worker subrequests · Cloudflare changelog
  2. Traces · Cloudflare Workers docs
  3. Spans and attributes · Cloudflare Workers docs
  4. Custom spans · Cloudflare Workers docs
  5. Known limitations · Cloudflare Workers tracing
  6. OpenTelemetry export · Cloudflare Observability docs
  7. OpenTelemetry export · Cloudflare Workers docs
  8. Local Explorer · Cloudflare Workers docs
  9. Introducing Cloudflare Traces · Cloudflare Blog
  10. Announcing Workers automatic tracing, now in open beta · Cloudflare Blog
  11. Polylane full content
  12. Polylane documentation

About the author

Boris Tane

Founder of Polylane

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

Platform playbooks

  1. 1 How to Monitor a Django App on Render
  2. 2 How to Monitor a FastAPI App on Railway: Logs, Traces and Alerts
  3. 3 How to Monitor a Supabase App in Production
  4. 4 How to Debug Cloudflare Workers Errors: Logs, Traces and Error Codes
  5. 5 How to debug Vercel function timeouts
  6. 6 Vercel 504 Gateway Timeout on Serverless Functions: Causes and Fixes
  7. 7 Cloudflare Workers error 1101: causes and how to fix it
  8. 8 Cloudflare Hyperdrive connection errors: causes and fixes
  9. 9 How to Monitor a Convex App in Production
  10. 10 How to monitor Cloudflare Durable Objects in production
  11. 11 How to roll back a Cloudflare Workers deployment
  12. 12 How to trace requests across Cloudflare Workers and service bindings
  13. 13 How to Monitor Vercel Cron Jobs: Heartbeats, Logs and Alerts
  14. 14 How to reduce Vercel Function cold starts
  15. 15 How to Monitor a Next.js App on Render
  16. 16 How to debug Modal function timeouts
  17. 17 Modal container out of memory: find the cause and fix it
  18. 18 Fix Railway “Healthcheck failed!” on deploy
  19. 19 How to Search Railway Logs for Errors: Filters, CLI and HTTP Logs

Related

Nobody should be on-call. Polylane watches your infra, finds what broke, and writes the fix.

Get started for free