Tracing on AWS with OpenTelemetry (X-Ray SDK replacement)

The handoff from application code to AWS X-Ray now that the X-Ray SDKs and daemon are in maintenance mode (since 2026-02-25) — instrument Node.js with OpenTelemetry or ADOT, export OTLP to the CloudWatch agent or an OpenTelemetry Collector, keep X-Ray propagation and annotations, map each X-Ray SDK call to its OpenTelemetry equivalent, and trace Lambda with the OpenTelemetry layer.

markdownpatternv4NYXR-VJ6Tstage: buildtrust: officialservice: xrayservice: cloudwatchservice: lambdainterface: sdkX-Ray SDK maintenance modeX-Ray daemon end of supportmigrate X-Ray to OpenTelemetryADOT Node.jsOpenTelemetry Node.js AWSNodeSDKOTLPTraceExporter 4318CloudWatch agent OTLP tracesOpenTelemetry Collector awsxray exporterAWSXRayPropagatorcaptureAWSv3Client replacementAwsInstrumentationaws.xray.annotationsstartActiveSpan segment subsegmentLambda OpenTelemetry layerApplication SignalsOTEL_TRACES_SAMPLER xray

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

Tracing on AWS with OpenTelemetry

Don't start new work on aws-xray-sdk. Since 2026-02-25 the X-Ray SDKs and daemon get security fixes only. Instrument with OpenTelemetry (or AWS's distribution of it, ADOT), and send OTLP to a CloudWatch agent or an OpenTelemetry Collector, which forwards to X-Ray. The X-Ray console and traces stay where they are; what changes is the code that produces them.

The handoff

app (OpenTelemetry SDK) ──OTLP http/protobuf :4318 or gRPC :4317──▶ CloudWatch agent ≥ 1.300025.0
                                                                     or OTel Collector (otlp receiver → awsxray exporter)
                                                                         ──▶ AWS X-Ray
  • CloudWatch agent: turn on OTLP trace collection (traces_collected.otlp, with grpc_endpoint on 4317 and http_endpoint on 4318).
  • OTel Collector: an otlp receiver and an awsxray exporter, plus the awsproxy extension if you want X-Ray sampling rules.
  • Mapping in X-Ray: server spans become segments, other spans subsegments. Attributes become metadata, except keys listed in aws.xray.annotations, which become searchable annotations.
  • Propagation: W3C Trace Context is OpenTelemetry's default. Add the X-Ray propagator where requests pass through X-Ray-enabled AWS services.

Node.js: replace the X-Ray SDK

X-Ray SDKOpenTelemetry
captureAWSv3Client(client)AwsInstrumentation (@opentelemetry/instrumentation-aws-sdk)
captureHTTPsGlobal / captureHTTPsHttpInstrumentation (@opentelemetry/instrumentation-http)
Express openSegment / closeSegmentHttpInstrumentation + ExpressInstrumentation
new AWSXRay.Segment, captureFunc, manual modetracer.startActiveSpan(…) (SpanKind.SERVER → segment, SpanKind.INTERNAL → subsegment)
addAnnotationPut the key in the aws.xray.annotations attribute
Centralized sampling rulesOTEL_TRACES_SAMPLER=xray with ADOT auto-instrumentation only. Plain OpenTelemetry JS has no X-Ray remote sampler

instrumentation.ts, loaded before the app (node --require ./instrumentation.js app.js):

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { AWSXRayPropagator } from "@opentelemetry/propagator-aws-xray";
import { awsEcsDetector, awsEc2Detector } from "@opentelemetry/resource-detector-aws";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { AwsInstrumentation } from "@opentelemetry/instrumentation-aws-sdk";

const sdk = new NodeSDK({
  serviceName: "orders-api",
  traceExporter: new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }), // agent or collector
  textMapPropagator: new AWSXRayPropagator(),
  resourceDetectors: [awsEcsDetector, awsEc2Detector],
  instrumentations: [new HttpInstrumentation(), new AwsInstrumentation()],
});
sdk.start();
process.on("SIGTERM", () => { void sdk.shutdown(); });

A custom span with an annotation:

import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("orders");

export async function chargeCard(orderId: string, amountCents: number) {
  return tracer.startActiveSpan("chargeCard", { kind: SpanKind.INTERNAL }, async (span) => {
    span.setAttributes({ orderId, amountCents, "aws.xray.annotations": ["orderId"] });
    try {
      return "ok"; // call the payment provider here
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

To auto-instrument everything instead, use @opentelemetry/auto-instrumentations-node, narrowed with OTEL_NODE_ENABLED_INSTRUMENTATIONS / OTEL_NODE_DISABLED_INSTRUMENTATIONS. Or use ADOT's Node.js auto-instrumentation, which also supports X-Ray sampling.

On ECS, run the CloudWatch agent or a collector as a sidecar and point the exporter at it. The guide's own example uses http://cwagent:4318/v1/traces. The task role then needs to write to X-Ray, which the hub hasn't collected; check the agent's documentation.

Lambda

  • Recommended: the AWS Lambda Layer for OpenTelemetry. It enables Application Signals by default; set OTEL_AWS_APPLICATION_SIGNALS_ENABLED=false for tracing only.
  • Alternative: the AWS managed ADOT JavaScript layer.
  • For manual spans next to the layer's automatic ones, add @opentelemetry/api at the version the layer's SDK uses. A mismatched version can produce spans that go nowhere (the hub's reading).

Known-good, known-bad, and which side is wrong

Known-good:

  • instrumentation.js loads with --require before the app's modules.
  • OTLP goes to a running CloudWatch agent (≥ 1.300025.0) or a collector with an awsxray exporter.
  • The X-Ray propagator is set.
  • Annotations are listed in aws.xray.annotations.
Known-badSymptom
Instrumentation imported after the app (or after @aws-sdk/* clients are created)Few or no AWS SDK and HTTP spans. Auto-instrumentation patches modules as they load (the hub's reading of --require ordering)
Exporter URL points at a port nothing listens on (4318 vs 4317 mismatch, sidecar missing)No traces in X-Ray; export errors in the app's logs
Expecting X-Ray sampling rules with plain OpenTelemetry JSSampling ignores the console rules: OTEL_TRACES_SAMPLER=xray works only with ADOT auto-instrumentation
Keys set as span attributes but not listed in aws.xray.annotationsTraces arrive, but you can't filter on those keys (they are metadata)
Manual spans with an @opentelemetry/api version different from the Lambda layer'sManual spans missing next to automatic ones (the hub's reading)

Which side is wrong:

  • No traces at all → the export side (endpoint, agent or collector, permissions).
  • Traces but missing AWS SDK spans → the instrumentation side (load order, instrumentations list).
  • Traces but broken across services → propagation (X-Ray propagator not configured where requests pass through X-Ray-enabled services).

What this doesn't change

  • Logs and metrics: reference/cloudwatch/log-groups.md, reference/cloudwatch/metrics-and-math.md.
  • When a Lambda log line contains an X-Ray trace ID, Logs Insights exposes it as @xrayTraceId. Use that to go from a slow invocation to its trace (reference/cloudwatch/logs-insights-queries.md).

Read this document as markdown — the same bytes an agent receives.