Skip to content

Cloudflare drops static child spans that end in waitUntil after the response #23427

Description

@betegon

Environment

  • Current develop, locally packed as @sentry/cloudflare 10.67.0
  • Wrangler 4.123.0
  • Cloudflare compatibility date 2026-08-13
  • traceLifecycle: 'static'

Problem

In the static trace lifecycle, the request transaction is assembled when the Worker handler returns. A child span which belongs to that request but ends later inside ExecutionContext.waitUntil() is not part of that snapshot and is never emitted independently.

The background work completes successfully and the Sentry client is still available. Only the span is lost.

This is distinct from #22545: the work is registered with waitUntil, rather than running as an untracked detached continuation.

Minimal reproduction

export default Sentry.withSentry(
  env => ({
    dsn: env.SENTRY_DSN,
    tracesSampleRate: 1,
    traceLifecycle: 'static',
  }),
  {
    async fetch(_request, _env, context) {
      const lateSpan = Sentry.startInactiveSpan({
        name: 'late waitUntil child',
        op: 'test.wait_until',
      });

      context.waitUntil(
        new Promise(resolve => {
          setTimeout(() => {
            lateSpan.end();
            resolve();
          }, 25);
        }),
      );

      return new Response(null, { status: 204 });
    },
  },
);

Actual behavior

Only the HTTP transaction is sent. The late child is absent from both the transaction and the remainder of the trace.

The behavior was reproduced locally from an exact SDK build and on a deployed Worker:

  • A standard MCP request produced its HTTP, MCP, and child spans.
  • A generic endpoint produced only its HTTP root; its late waitUntil child was missing.
  • A modern MCP 2026-07-28 request produced its HTTP and MCP spans; its late tool child was missing.

The generic endpoint confirms that this is a Cloudflare static-lifecycle problem, not an MCP protocol regression.

Expected behavior

A late child should be represented exactly once. If its segment transaction was already accepted, it should be emitted as a standalone transaction on the same trace, retaining its exact original parent and carrying sentry.parent_span_already_sent=true.

There must be no embedded-plus-standalone duplicate.

Additional context

Modern MCP 2026-07-28 exposed the problem because its stateless request can finish before background tool work. It is only a reproduction workload and control.

The legacy McpAgent path does not expose the same baseline because its tool spans use forceTransaction, making them independently capturable and masking this lifecycle gap.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions