Custom spans·续

await tracing.startActiveSpan("chargeCard", async (span) => {
    try {
        const result = await chargeCard(token, amount);
        span.setStatus({ code: "ok" });
        return result;
    } catch (err) {
        span.recordException(err as Error);
        span.setStatus({ code: "error", message: "Card payment failed" });
        throw err;
    } finally {
        span.end();
    }
});

Span status is included in traces that you export to an OpenTelemetry destination. The Workers Observability dashboard does not display span status yet, and you cannot query or filter traces by span status.

span.updateName(name)

Changes the name of the span. Use this when the best name for an operation is not known until after the span starts, such as after you match a request to a route.

Parameter Type Description
name string The new name of the span. Like names passed to the span creation methods, it is truncated to 64 bytes.

Returns: The same Span, so you can chain calls.

Behavior:

  • The new name replaces the original name everywhere the span appears, including in exported traces.
  • Calls after the span has ended are ignored. The span keeps the last name it had before end() was called.
  • You can also rename the invocation's root span returned by getActiveSpan().
// Rename the invocation's root span to include the matched route
const route = router.match(request);
tracing.getActiveSpan()?.updateName(`${request.method} ${route.pattern}`);

const span = tracing.startSpan("job");
span.updateName(`job ${job.type}`);
await runJob(job);
span.end();

span.updateName("ignored"); // No effect, the span has already ended

span.spanContext()

Returns the identity of the span: its trace ID, span ID, and trace flags. The fields match the required fields of an OpenTelemetry SpanContext ↗︎.

Returns: A SpanContext object with the following fields:

Field Type Description
traceId string The trace ID, as 32 lowercase hexadecimal characters. Every span in an invocation has the same trace ID.
spanId string The span ID, as 16 lowercase hexadecimal characters.
traceFlags number The W3C trace flags ↗︎ for the span.

Behavior:

  • The trace ID and span ID are the same IDs that appear for the trace and span in Workers Observability and in traces exported to OpenTelemetry destinations.
  • spanContext() still returns the span's IDs after the span has ended.
  • For the root span returned by getActiveSpan() outside any custom span, spanContext() returns the invocation's trace ID and root span ID.
  • If observability is disabled for the Worker, traceId and spanId are all zeros ( "00000000000000000000000000000000" and "0000000000000000").
  • If traces are disabled or the request is not sampled for tracing, but Workers Logs is enabled, spanContext() still returns non-zero IDs. The trace ID matches the one attached to the invocation's logs, but no spans are stored for it.
  • traceFlags does not currently indicate whether the request is sampled for tracing. Do not use it to decide whether to record telemetry, or to build a traceparent header for downstream services.

Use spanContext() to correlate a request with its trace, for example by logging the trace ID or returning it to the client:

src/index.jsjs

import { tracing } from "cloudflare:workers";

export default {
    async fetch(request, env, ctx) {
        const { traceId } = tracing.getActiveSpan()?.spanContext() ?? {};

const response = await handleRequest(request, env);

// Return the trace ID so that it can be looked up later
        const headers = new Headers(response.headers);
        if (traceId) headers.set("x-trace-id", traceId);
        return new Response(response.body, { status: response.status, headers });
    },
};

src/index.tsts

import { tracing } from "cloudflare:workers";

export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext) {
        const { traceId } = tracing.getActiveSpan()?.spanContext() ?? {};

const response = await handleRequest(request, env);

// Return the trace ID so that it can be looked up later
        const headers = new Headers(response.headers);
        if (traceId) headers.set("x-trace-id", traceId);
        return new Response(response.body, { status: response.status, headers });
    },
};

span.isTraced

A readonly boolean indicating whether this invocation is being traced. isTraced is false when observability is disabled for the Worker, and after the span has ended. When isTraced is false, enterSpan still runs the callback but does not record any telemetry.

isTraced does not currently account for head sampling. A request that is not sampled for tracing can still report true, and its spans are discarded after the invocation.

You can use this to skip expensive attribute computation when the request is not being traced:

tracing.enterSpan("process", (span) => {
    if (span.isTraced) {
        span.setAttribute(
            "request.body.preview",
            JSON.stringify(body).slice(0, 200),
        );
    }
    return processBody(body);
});

span.end()

Ends the span and submits its attributes to the tracing system. This method is idempotent. Calling it multiple times has no effect after the first call. After end() is called, span.isTraced returns false and any further method calls to annotate the span are silently ignored, including calls from in-flight async work that has not yet completed.

  • For spans created with enterSpan, you do not need to call end(). The runtime calls it automatically. Calling end() yourself is safe, but could end the span early.
  • For spans created with startActiveSpan or startSpan, you must call end() to submit the span.
  • For the root span returned by getActiveSpan() outside any custom span, end() has no effect. The runtime ends the root span when the invocation completes.
const span = tracing.startSpan("manual-op");
span.setAttribute("step", "processing");
await doWork();

// Later, when the work is truly complete:
span.end(); // Span is submitted
span.end(); // No-op, safe to call again

Nested spans

Spans nest automatically based on the JavaScript async context. Any enterSpan call or platform operation (such as fetch and env.MY_KV.get()) that runs inside a callback becomes a child of the enclosing span.

src/index.jsjs

import { tracing } from "cloudflare:workers";

async function handleOrder(env, orderId) {
    return tracing.enterSpan("handleOrder", async (span) => {
        span.setAttribute("order.id", orderId);

// This KV read is automatically a child of "handleOrder"
        const order = await env.ORDERS_KV.get(orderId, "json");

// This nested span is also a child of "handleOrder"
        const total = tracing.enterSpan("calculateTotal", (innerSpan) => {
            innerSpan.setAttribute("item.count", order.items.length);
            return order.items.reduce((sum, item) => sum + item.price, 0);
        });

// This fetch is a child of "handleOrder"
        await fetch("https://api.example.com/notify", {
            method: "POST",
            body: JSON.stringify({ orderId, total }),
        });

return new Response(JSON.stringify({ orderId, total }));
    });
}

src/index.tsts

import { tracing } from "cloudflare:workers";

async function handleOrder(env: Env, orderId: string) {
    return tracing.enterSpan("handleOrder", async (span) => {
        span.setAttribute("order.id", orderId);

// This KV read is automatically a child of "handleOrder"
        const order = await env.ORDERS_KV.get(orderId, "json");

// This nested span is also a child of "handleOrder"
        const total = tracing.enterSpan("calculateTotal", (innerSpan) => {
            innerSpan.setAttribute("item.count", order.items.length);
            return order.items.reduce(
                (sum: number, item: any) => sum + item.price,
                0,
            );
        });

// This fetch is a child of "handleOrder"
        await fetch("https://api.example.com/notify", {
            method: "POST",
            body: JSON.stringify({ orderId, total }),
        });

return new Response(JSON.stringify({ orderId, total }));
    });
}

Trace waterfall showing custom spans nested alongside automatic KV and fetch instrumentation

Logging within spans

console.log() and other console methods emit log events that are automatically attributed to the currently active span. This means log output from inside an enterSpan or startActiveSpan callback is associated with that span in your traces and OpenTelemetry exports.

tracing.enterSpan("processPayment", async (span) => {
    console.log("Starting payment processing"); // attributed to "processPayment"
    const result = await chargeCard(token, amount);
    console.log("Payment complete", result.id); // also attributed to "processPayment"
});

TypeScript types

The full type declarations for the custom spans API:

declare module "cloudflare:workers" {
    namespace tracing {
        function enterSpan<T, A extends unknown[]>(
            name: string,
            callback: (span: Span, ...args: A) => T,
            ...args: A
        ): T;

function startActiveSpan<T, A extends unknown[]>(
            name: string,
            callback: (span: Span, ...args: A) => T,
            ...args: A
        ): T;

function startSpan(name: string): Span;

function getActiveSpan(): Span | undefined;
    }

type Exception =
        | string
        | { code: string | number; name?: string; message?: string; stack?: string }
        | { code?: string | number; name: string; message?: string; stack?: string }
        | { code?: string | number; name?: string; message: string; stack?: string };

type SpanStatus = {
        code: "unset" | "ok" | "error";
        /** A developer-facing error message. Ignored unless code is "error". */
        message?: string;
    };

type SpanContext = {
        traceId: string;
        spanId: string;
        traceFlags: number;
    };

class Span {
        readonly isTraced: boolean;
        setAttribute(key: string, value: string | number | boolean): this;
        setAttributes(
            attributes: Record<string, string | number | boolean | undefined>,
        ): this;
        recordException(exception: Exception): void;
        setStatus(status: SpanStatus): this;
        updateName(name: string): this;
        spanContext(): SpanContext;
        end(): void;
    }
}

The same API is available on the handler context as ctx.tracing, with the same types.

Choosing a span creation method

enterSpan startActiveSpan startSpan
Span ends Automatically, when the callback returns, throws, or its returned promise settles Manually, when you call span.end() Manually, when you call span.end()
Active context scope During the callback During the callback Never active
Use case Most instrumentation — sync and async work that fits within a single callback Operations that outlive the callback, such as stream pipelines Timing an operation with no nested spans, when you manage the start and end yourself
Error handling Span auto-ends on throw Span stays open on throw. Call span.end() or rely on the runtime backstop Span stays open on throw. Call span.end() or rely on the runtime backstop

enterSpan and startActiveSpan set the span as the active context parent only during the callback. After the callback returns, the span is no longer the active parent. With enterSpan, this distinction does not matter because the span is also ended. With startActiveSpan, the span remains open but is no longer the context parent — new spans created after the callback returns are not children of this span. startSpan never sets the span as the active context parent, so no spans are ever created as its children.

Data limits

The runtime limits how much data you can add to a custom span:

  • Span names are truncated to 64 bytes.
  • Each span can hold approximately 64 KB of attribute and exception data. Attribute keys and values, and the code, name, message, and stack of recorded exceptions, all count toward this limit.

After a span reaches its data limit, the runtime ignores further setAttribute, setAttributes, and recordException calls on that span. It adds two attributes to the span so you can tell that data was dropped:

Attribute Value
cloudflare.warning.type span_data_limit_exceeded
cloudflare.warning.message A description of the attribute or exception that was dropped, and its size.

Limitations

  • No manual parent-child wiring. Parent-child relationships are determined by the JavaScript async context automatically.
  • Sampling decisions are not visible to your code. span.isTraced and spanContext().traceFlags do not reflect head sampling. Trace context is also not propagated automatically. Refer to known limitations.
  • Span status is not shown in the dashboard. Status set with setStatus() is included in exported traces, but the Workers Observability dashboard does not display it yet, and you cannot query or filter by it.

For other tracing limitations, refer to the known limitations page.

Was this helpful?

YesNo

On this page

Custom spans·续 - 图2Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/observability/traces/custom-spans/#page","headline":"Custom spans","description":"Create custom spans to trace your own application logic alongside Cloudflare's automatic instrumentation.","url":"https://developers.cloudflare.com/workers/observability/traces/custom-spans/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/observability/traces/custom-spans/og.png?v=72e1fb08b62a6c1f","dateModified":"2026-10-26","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Workers tracing is currently in open beta. This page documents current limitations and any upcoming features on our roadmap. title: Known limitations image: https://developers.cloudflare.com/workers/observability/traces/known-limitations/og.png?v=22170f4759c61975


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.