diff --git a/src/content/docs/distributed-tracing/ui-data/trace-details.mdx b/src/content/docs/distributed-tracing/ui-data/trace-details.mdx
index c37525c82b9..47dcdbd80ac 100644
--- a/src/content/docs/distributed-tracing/ui-data/trace-details.mdx
+++ b/src/content/docs/distributed-tracing/ui-data/trace-details.mdx
@@ -347,12 +347,154 @@ Check out ways to display your traces' spans:
When you select a span, a pane opens up with span details. These details can be helpful for troubleshooting performance issues. This page has three tabs:
-* **Performance**: You’ll see charts showing the average duration and throughput for that span operation, as well as how the performance of this specific span compares to the average.
+* **Performance**: You'll see charts showing the average duration and throughput for that span operation, as well as how the performance of this specific span compares to the average.
* **Attributes**: You can learn more about attributes from our [data dictionary](/attribute-dictionary/?dataSource=Distributed+Tracing&event=Span).
* **Details**: You can view details, such as the [span's full name](/docs/distributed-tracing/ui-data/understand-use-distributed-tracing-ui/#prettified-span-names) and data source.
+* **Span links**: If the span has [span links](#span-links), you'll see this tab. It shows causal relationships with spans from other traces. [Learn more about span links](#span-links).
What a span displays is based on its span type. For example, the datastore span's details will include the database query. For more on the trace structure and how span properties are determined, see [Trace structure](/docs/understand-dependencies/distributed-tracing/get-started/how-new-relic-distributed-tracing-works#trace-structure).
+## Understanding span links [#span-links]
+
+Span links help you understand causal relationships between spans across different traces. When traces get split due to asynchronous communication or long-running processes, span links allow you to navigate between related traces and see the complete picture of your distributed system.
+
+### When traces get split [#why-traces-split]
+
+In modern distributed systems, traces can appear as separate, disconnected traces in the following scenarios:
+
+* **Asynchronous message queues**: A service publishes messages to a queue (like AWS SQS, RabbitMQ, or Kafka), and another service consumes them later. The producing trace and consuming trace are separate, but causally related.
+* **Long-running workflows**: Background jobs or workflows that exceed trace duration limits or run for extended periods.
+* **Batch processing**: Workers that process multiple messages in batches, where each message originated from a different trace.
+* **Fan-in patterns**: Multiple services independently produce messages that one consumer aggregates. Each producer creates its own trace, and the consumer creates a new trace that processes all of them.
+
+When traces are split like this, traditional parent-child relationships don't capture the full picture. Span links fill this gap by creating explicit causal connections.
+
+### What are span links? [#what-are-span-links]
+
+Span links are OpenTelemetry attributes that create causal relationships between spans that don't have a direct parent-child connection. They allow you to:
+
+* Navigate from a consuming span back to the producing span
+* Understand the complete request flow even when traces are split
+* Debug issues that span across asynchronous boundaries
+* Visualize dependencies in event-driven architectures
+
+A span link includes the trace ID and span ID of the related span, allowing New Relic to connect the traces for you.
+
+### Identifying traces with span links [#identify-span-links]
+
+In the trace details UI, traces with span links are marked in several ways:
+
+1. **Span links filter badge**: In the trace filter bar, you'll see a badge showing the number of spans with links (for example, `span links (2)`).
+2. **Span count indicator**: When viewing the list of spans, each span shows a link count if it has span links.
+
+
+
+
+ The span links filter badge appears in the trace filter bar when a trace contains spans with links.
+
+
+### Viewing span links [#view-span-links]
+
+To view and navigate span links:
+
+1. From the trace details page, select a span that has span links (look for the span links indicator).
+2. In the span details pane, click the **Span links** tab.
+3. You'll see a list of linked traces with the following information:
+ * **Direction badge**: `Forward` indicates a span that occurred later in time (successor), while `Backward` indicates a span that occurred earlier (predecessor).
+ * **Trace ID**: The ID of the linked trace (truncated for display).
+ * **Timestamp**: When the linked span occurred.
+ * **Duration**: How long the linked span took.
+ * **Errors**: Number of errors in the linked trace.
+
+4. Click on a linked trace to navigate to it. Use your browser's back button to return to the original trace.
+
+
+
+
+ The Span links tab displays linked traces with direction badges (Forward/Backward), trace IDs, timestamps, durations, and error counts.
+
+
+
+ **Understanding forward vs. backward links**
+
+ * **Forward links**: Point to traces that happened as a result of the current span. Example: A message was published by this span and processed later by another trace.
+ * **Backward links**: Point to traces that caused the current span. Example: This span is processing a message that was published by an earlier trace.
+
+
+### When span links don't appear [#span-links-troubleshooting]
+
+If you expect to see span links but they're not appearing, check the following:
+
+
+
+
+ When a trace has no span links, you'll see this message in the Span links tab.
+
+
+
+
+ Span links require proper trace context propagation through your messaging system:
+
+ * Ensure W3C trace context headers (`traceparent`, `tracestate`) are included in message headers or metadata
+ * Verify your messaging library or framework supports context propagation
+ * When instrumenting manually, make sure you're extracting context from messages and creating span links programmatically
+
+ For implementation examples, see [OpenTelemetry span links best practices](/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces/#span-links).
+
+
+
+ If your linked traces are in different New Relic accounts:
+
+ * Verify that trusted account IDs are configured correctly for cross-account tracing
+ * Check that all sub-accounts share the same parent account ID
+ * Ensure you have access to both accounts
+
+ Without proper cross-account configuration, you may see span link data but won't be able to navigate to the linked trace.
+
+
+
+ Both the original trace and the linked trace must be sampled for span links to appear:
+
+ * If either trace is dropped by sampling, the span link won't be visible
+ * Consider using head-based or tail-based sampling strategies that preserve important traces
+ * For critical workflows with span links, you may want to adjust sampling rates
+
+
+
+ There may be a delay between when trace data is sent and when span links appear:
+
+ * Allow 1-2 minutes for data to be processed and indexed
+ * Check that both linked traces have been successfully ingested
+ * Verify that you're not hitting any data limits or rate limits
+
+
+
+For more information on implementing span links in your instrumentation, see our [OpenTelemetry best practices guide for traces](/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces/#span-links).
+
## Span attributes [#span-attributes]
If you'd like to learn more about `Span` data:
diff --git a/src/content/docs/distributed-tracing/ui-data/understand-use-distributed-tracing-ui.mdx b/src/content/docs/distributed-tracing/ui-data/understand-use-distributed-tracing-ui.mdx
index a4daabfb59e..b5f0fb6c56a 100644
--- a/src/content/docs/distributed-tracing/ui-data/understand-use-distributed-tracing-ui.mdx
+++ b/src/content/docs/distributed-tracing/ui-data/understand-use-distributed-tracing-ui.mdx
@@ -29,6 +29,12 @@ If you're looking to troubleshoot errors in a transaction that spans many servic
3. On the [trace details page](/docs/distributed-tracing/ui-data/trace-details/), review the span along the request route that originated the error.
4. Noting the error class and message, navigate to the service from its span in the trace so you can see that the error is occurring at a high rate.
+
+ **Working with asynchronous traces?**
+
+ If your traces get split across asynchronous boundaries like message queues or event streams, you can use [span links](/docs/distributed-tracing/ui-data/trace-details/#span-links) to navigate between causally related traces. Span links help you understand the complete request flow even when traces are disconnected. Learn more about [understanding span links](/docs/distributed-tracing/ui-data/trace-details/#span-links).
+
+
Read on to explore the options in the distributed tracing UI.
## Open the distributed tracing UI [#open-dt-ui]
diff --git a/src/content/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces.mdx b/src/content/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces.mdx
index 61c508d6eb4..a5480369be4 100644
--- a/src/content/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces.mdx
+++ b/src/content/docs/opentelemetry/best-practices/opentelemetry-best-practices-traces.mdx
@@ -263,6 +263,683 @@ New Relic maps OTLP spans to the `Span` data type. The table below describes how
See [OTLP attribute types](/docs/opentelemetry/best-practices/opentelemetry-otlp/#otlp-attribute-types) for details on New Relic OTLP endpoint supported attribute types and [OTLP attribute limits](/docs/opentelemetry/best-practices/opentelemetry-otlp/#attribute-limits) for details on validation performed on attributes.
-## Span links support [#span-links-support]
+## Span links [#span-links]
+
+New Relic supports OpenTelemetry [span links](https://opentelemetry.io/docs/concepts/signals/traces/#span-links), which allow you to create causal relationships between spans that don't have a direct parent-child connection. Span links are essential for understanding distributed traces that get split across asynchronous boundaries like message queues, event streams, and batch processing systems.
+
+### When to use span links [#when-to-use]
+
+Use span links in the following scenarios:
+
+* **Message queue producers and consumers**: Link a consuming span back to the producing span when processing messages from queues like AWS SQS, RabbitMQ, or Kafka.
+* **Fan-in patterns**: Link multiple producer traces to a single consumer trace that aggregates their outputs.
+* **Batch processing**: Link spans that process batched messages back to their individual originating traces.
+* **Long-running workflows**: Connect spans across workflow steps that exceed normal trace duration limits.
+
+### Implementing span links [#implementing-span-links]
+
+To implement span links in your OpenTelemetry instrumentation, you need to:
+
+1. Extract trace context from the incoming message or event
+2. Create a span link when starting a new span in the consumer
+3. Ensure trace context is propagated through your messaging infrastructure
+
+The following examples show how to implement span links in different languages:
+
+
+
+ ```python
+ from opentelemetry import trace
+ from opentelemetry.trace import Link, SpanContext, TraceFlags
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
+
+ tracer = trace.get_tracer(__name__)
+ propagator = TraceContextTextMapPropagator()
+
+ # Producer: Publishing a message with trace context
+ def publish_message(queue, message_body):
+ with tracer.start_as_current_span("publish_message") as span:
+ # Prepare message with trace context headers
+ carrier = {}
+ propagator.inject(carrier)
+
+ # Add carrier headers to your message metadata
+ message = {
+ 'body': message_body,
+ 'headers': carrier
+ }
+
+ queue.publish(message)
+ span.set_attribute("messaging.destination", queue.name)
+ span.set_attribute("messaging.system", "custom_queue")
+
+ # Consumer: Processing a message with span link
+ def process_message(message):
+ # Extract trace context from message headers
+ carrier = message.get('headers', {})
+ ctx = propagator.extract(carrier)
+
+ # Get the span context from the extracted context
+ span_context = trace.get_current_span(ctx).get_span_context()
+
+ # Create a new span with a link to the producer span
+ links = []
+ if span_context.is_valid:
+ links = [Link(span_context)]
+
+ with tracer.start_as_current_span(
+ "process_message",
+ links=links
+ ) as span:
+ # Process the message
+ result = handle_message(message['body'])
+
+ span.set_attribute("messaging.system", "custom_queue")
+ span.set_attribute("messaging.operation", "process")
+
+ return result
+ ```
+
+ For AWS SQS specifically:
+ ```python
+ import boto3
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
+
+ sqs = boto3.client('sqs')
+ propagator = TraceContextTextMapPropagator()
+
+ # Publishing to SQS
+ def send_sqs_message(queue_url, message_body):
+ with tracer.start_as_current_span("sqs_publish") as span:
+ carrier = {}
+ propagator.inject(carrier)
+
+ # SQS message attributes for trace context
+ message_attributes = {
+ 'traceparent': {
+ 'StringValue': carrier.get('traceparent', ''),
+ 'DataType': 'String'
+ }
+ }
+
+ if 'tracestate' in carrier:
+ message_attributes['tracestate'] = {
+ 'StringValue': carrier['tracestate'],
+ 'DataType': 'String'
+ }
+
+ sqs.send_message(
+ QueueUrl=queue_url,
+ MessageBody=message_body,
+ MessageAttributes=message_attributes
+ )
+
+ # Consuming from SQS
+ def process_sqs_message(message):
+ # Extract trace context from SQS message attributes
+ carrier = {}
+ if 'MessageAttributes' in message:
+ attrs = message['MessageAttributes']
+ if 'traceparent' in attrs:
+ carrier['traceparent'] = attrs['traceparent']['StringValue']
+ if 'tracestate' in attrs:
+ carrier['tracestate'] = attrs['tracestate']['StringValue']
+
+ ctx = propagator.extract(carrier)
+ span_context = trace.get_current_span(ctx).get_span_context()
+
+ links = [Link(span_context)] if span_context.is_valid else []
+
+ with tracer.start_as_current_span(
+ "sqs_process",
+ links=links
+ ) as span:
+ # Process message
+ body = message['Body']
+ return handle_message(body)
+ ```
+
+
+
+ ```java
+ import io.opentelemetry.api.trace.Span;
+ import io.opentelemetry.api.trace.SpanBuilder;
+ import io.opentelemetry.api.trace.SpanContext;
+ import io.opentelemetry.api.trace.Tracer;
+ import io.opentelemetry.context.Context;
+ import io.opentelemetry.context.propagation.TextMapGetter;
+ import io.opentelemetry.context.propagation.TextMapPropagator;
+ import io.opentelemetry.context.propagation.TextMapSetter;
+
+ public class MessageProcessor {
+ private final Tracer tracer;
+ private final TextMapPropagator propagator;
+
+ // Producer: Publishing a message with trace context
+ public void publishMessage(Queue queue, String messageBody) {
+ Span span = tracer.spanBuilder("publish_message")
+ .startSpan();
+
+ try (var scope = span.makeCurrent()) {
+ // Inject trace context into message headers
+ Map headers = new HashMap<>();
+ propagator.inject(Context.current(), headers,
+ (carrier, key, value) -> carrier.put(key, value));
+
+ Message message = new Message(messageBody, headers);
+ queue.publish(message);
+
+ span.setAttribute("messaging.destination", queue.getName());
+ span.setAttribute("messaging.system", "custom_queue");
+ } finally {
+ span.end();
+ }
+ }
+
+ // Consumer: Processing a message with span link
+ public void processMessage(Message message) {
+ // Extract trace context from message headers
+ Context extractedContext = propagator.extract(
+ Context.current(),
+ message.getHeaders(),
+ (carrier, key) -> carrier.get(key)
+ );
+
+ // Get the span context from extracted context
+ SpanContext producerSpanContext = Span.fromContext(extractedContext)
+ .getSpanContext();
+
+ // Create span with link to producer
+ SpanBuilder spanBuilder = tracer.spanBuilder("process_message");
+
+ if (producerSpanContext.isValid()) {
+ spanBuilder.addLink(producerSpanContext);
+ }
+
+ Span span = spanBuilder.startSpan();
+
+ try (var scope = span.makeCurrent()) {
+ handleMessage(message.getBody());
+
+ span.setAttribute("messaging.system", "custom_queue");
+ span.setAttribute("messaging.operation", "process");
+ } finally {
+ span.end();
+ }
+ }
+
+ // AWS SQS example
+ public void processSQSMessage(
+ software.amazon.awssdk.services.sqs.model.Message sqsMessage
+ ) {
+ Map carrier = new HashMap<>();
+
+ // Extract trace context from SQS message attributes
+ sqsMessage.messageAttributes().forEach((key, value) -> {
+ if (key.equals("traceparent") || key.equals("tracestate")) {
+ carrier.put(key, value.stringValue());
+ }
+ });
+
+ Context extractedContext = propagator.extract(
+ Context.current(),
+ carrier,
+ (c, k) -> c.get(k)
+ );
+
+ SpanContext producerSpanContext = Span.fromContext(extractedContext)
+ .getSpanContext();
+
+ SpanBuilder spanBuilder = tracer.spanBuilder("sqs_process");
+
+ if (producerSpanContext.isValid()) {
+ spanBuilder.addLink(producerSpanContext);
+ }
+
+ Span span = spanBuilder.startSpan();
+
+ try (var scope = span.makeCurrent()) {
+ handleMessage(sqsMessage.body());
+ span.setAttribute("messaging.system", "AmazonSQS");
+ } finally {
+ span.end();
+ }
+ }
+ }
+ ```
+
+
+
+ ```javascript
+ const { trace, context, SpanKind } = require('@opentelemetry/api');
+ const { W3CTraceContextPropagator } = require('@opentelemetry/core');
+
+ const tracer = trace.getTracer('message-processor');
+ const propagator = new W3CTraceContextPropagator();
+
+ // Producer: Publishing a message with trace context
+ async function publishMessage(queue, messageBody) {
+ const span = tracer.startSpan('publish_message', {
+ kind: SpanKind.PRODUCER
+ });
+
+ return context.with(trace.setSpan(context.active(), span), async () => {
+ try {
+ // Inject trace context into message headers
+ const carrier = {};
+ propagator.inject(
+ context.active(),
+ carrier,
+ {
+ set: (carrier, key, value) => {
+ carrier[key] = value;
+ }
+ }
+ );
+
+ const message = {
+ body: messageBody,
+ headers: carrier
+ };
+
+ await queue.publish(message);
+
+ span.setAttribute('messaging.destination', queue.name);
+ span.setAttribute('messaging.system', 'custom_queue');
+ } finally {
+ span.end();
+ }
+ });
+ }
+
+ // Consumer: Processing a message with span link
+ async function processMessage(message) {
+ // Extract trace context from message headers
+ const extractedContext = propagator.extract(
+ context.active(),
+ message.headers || {},
+ {
+ get: (carrier, key) => carrier[key]
+ }
+ );
+
+ // Get the span context from extracted context
+ const producerSpan = trace.getSpan(extractedContext);
+ const producerSpanContext = producerSpan?.spanContext();
+
+ // Create span with link
+ const links = [];
+ if (producerSpanContext && trace.isSpanContextValid(producerSpanContext)) {
+ links.push({
+ context: producerSpanContext
+ });
+ }
+
+ const span = tracer.startSpan(
+ 'process_message',
+ {
+ kind: SpanKind.CONSUMER,
+ links: links
+ }
+ );
+
+ return context.with(trace.setSpan(context.active(), span), async () => {
+ try {
+ await handleMessage(message.body);
+
+ span.setAttribute('messaging.system', 'custom_queue');
+ span.setAttribute('messaging.operation', 'process');
+ } finally {
+ span.end();
+ }
+ });
+ }
+
+ // AWS SQS example using AWS SDK v3
+ const { SQSClient, SendMessageCommand, ReceiveMessageCommand } = require('@aws-sdk/client-sqs');
+
+ async function sendSQSMessage(queueUrl, messageBody) {
+ const span = tracer.startSpan('sqs_publish');
+
+ return context.with(trace.setSpan(context.active(), span), async () => {
+ try {
+ const carrier = {};
+ propagator.inject(context.active(), carrier, {
+ set: (c, k, v) => { c[k] = v; }
+ });
+
+ const messageAttributes = {
+ traceparent: {
+ StringValue: carrier.traceparent || '',
+ DataType: 'String'
+ }
+ };
+
+ if (carrier.tracestate) {
+ messageAttributes.tracestate = {
+ StringValue: carrier.tracestate,
+ DataType: 'String'
+ };
+ }
+
+ const client = new SQSClient({});
+ await client.send(new SendMessageCommand({
+ QueueUrl: queueUrl,
+ MessageBody: messageBody,
+ MessageAttributes: messageAttributes
+ }));
+ } finally {
+ span.end();
+ }
+ });
+ }
+
+ async function processSQSMessage(message) {
+ const carrier = {};
+
+ if (message.MessageAttributes) {
+ if (message.MessageAttributes.traceparent) {
+ carrier.traceparent = message.MessageAttributes.traceparent.StringValue;
+ }
+ if (message.MessageAttributes.tracestate) {
+ carrier.tracestate = message.MessageAttributes.tracestate.StringValue;
+ }
+ }
+
+ const extractedContext = propagator.extract(context.active(), carrier, {
+ get: (c, k) => c[k]
+ });
+
+ const producerSpanContext = trace.getSpan(extractedContext)?.spanContext();
+ const links = producerSpanContext && trace.isSpanContextValid(producerSpanContext)
+ ? [{ context: producerSpanContext }]
+ : [];
+
+ const span = tracer.startSpan('sqs_process', { links });
+
+ return context.with(trace.setSpan(context.active(), span), async () => {
+ try {
+ await handleMessage(message.Body);
+ span.setAttribute('messaging.system', 'AmazonSQS');
+ } finally {
+ span.end();
+ }
+ });
+ }
+ ```
+
+
+
+ ```go
+ package main
+
+ import (
+ "context"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/propagation"
+ "go.opentelemetry.io/otel/trace"
+ )
+
+ var (
+ tracer = otel.Tracer("message-processor")
+ propagator = propagation.TraceContext{}
+ )
+
+ // Producer: Publishing a message with trace context
+ func publishMessage(ctx context.Context, queue Queue, messageBody string) error {
+ ctx, span := tracer.Start(ctx, "publish_message")
+ defer span.End()
+
+ // Inject trace context into message headers
+ carrier := propagation.MapCarrier{}
+ propagator.Inject(ctx, carrier)
+
+ message := Message{
+ Body: messageBody,
+ Headers: map[string]string(carrier),
+ }
+
+ err := queue.Publish(message)
+
+ span.SetAttributes(
+ attribute.String("messaging.destination", queue.Name()),
+ attribute.String("messaging.system", "custom_queue"),
+ )
+
+ return err
+ }
+
+ // Consumer: Processing a message with span link
+ func processMessage(ctx context.Context, message Message) error {
+ // Extract trace context from message headers
+ carrier := propagation.MapCarrier(message.Headers)
+ extractedCtx := propagator.Extract(ctx, carrier)
+
+ // Get the span context from extracted context
+ producerSpanContext := trace.SpanContextFromContext(extractedCtx)
+
+ // Create span with link
+ var links []trace.Link
+ if producerSpanContext.IsValid() {
+ links = []trace.Link{
+ {
+ SpanContext: producerSpanContext,
+ },
+ }
+ }
+
+ ctx, span := tracer.Start(
+ ctx,
+ "process_message",
+ trace.WithLinks(links...),
+ )
+ defer span.End()
+
+ err := handleMessage(message.Body)
+
+ span.SetAttributes(
+ attribute.String("messaging.system", "custom_queue"),
+ attribute.String("messaging.operation", "process"),
+ )
+
+ return err
+ }
+
+ // AWS SQS example
+ func processSQSMessage(ctx context.Context, sqsMessage *sqs.Message) error {
+ // Extract trace context from SQS message attributes
+ carrier := propagation.MapCarrier{}
+
+ if sqsMessage.MessageAttributes != nil {
+ if tp, ok := sqsMessage.MessageAttributes["traceparent"]; ok {
+ carrier["traceparent"] = *tp.StringValue
+ }
+ if ts, ok := sqsMessage.MessageAttributes["tracestate"]; ok {
+ carrier["tracestate"] = *ts.StringValue
+ }
+ }
+
+ extractedCtx := propagator.Extract(ctx, carrier)
+ producerSpanContext := trace.SpanContextFromContext(extractedCtx)
+
+ var links []trace.Link
+ if producerSpanContext.IsValid() {
+ links = []trace.Link{
+ {SpanContext: producerSpanContext},
+ }
+ }
+
+ ctx, span := tracer.Start(
+ ctx,
+ "sqs_process",
+ trace.WithLinks(links...),
+ )
+ defer span.End()
+
+ err := handleMessage(*sqsMessage.Body)
+
+ span.SetAttributes(
+ attribute.String("messaging.system", "AmazonSQS"),
+ )
+
+ return err
+ }
+ ```
+
+
+
+ ```csharp
+ using System.Diagnostics;
+ using OpenTelemetry;
+ using OpenTelemetry.Context.Propagation;
+
+ public class MessageProcessor
+ {
+ private static readonly ActivitySource ActivitySource = new("MessageProcessor");
+ private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator;
+
+ // Producer: Publishing a message with trace context
+ public async Task PublishMessage(IQueue queue, string messageBody)
+ {
+ using var activity = ActivitySource.StartActivity("publish_message", ActivityKind.Producer);
+
+ // Inject trace context into message headers
+ var carrier = new Dictionary();
+ Propagator.Inject(
+ new PropagationContext(activity.Context, Baggage.Current),
+ carrier,
+ (c, key, value) => c[key] = value
+ );
+
+ var message = new Message
+ {
+ Body = messageBody,
+ Headers = carrier
+ };
+
+ await queue.PublishAsync(message);
+
+ activity?.SetTag("messaging.destination", queue.Name);
+ activity?.SetTag("messaging.system", "custom_queue");
+ }
+
+ // Consumer: Processing a message with span link
+ public async Task ProcessMessage(Message message)
+ {
+ // Extract trace context from message headers
+ var propagationContext = Propagator.Extract(
+ default,
+ message.Headers ?? new Dictionary(),
+ (carrier, key) => carrier.TryGetValue(key, out var value) ? new[] { value } : Array.Empty()
+ );
+
+ var producerContext = propagationContext.ActivityContext;
+
+ // Create span with link
+ var links = new List();
+ if (producerContext != default)
+ {
+ links.Add(new ActivityLink(producerContext));
+ }
+
+ using var activity = ActivitySource.StartActivity(
+ "process_message",
+ ActivityKind.Consumer,
+ parentContext: default,
+ links: links
+ );
+
+ await HandleMessage(message.Body);
+
+ activity?.SetTag("messaging.system", "custom_queue");
+ activity?.SetTag("messaging.operation", "process");
+ }
+
+ // AWS SQS example
+ public async Task ProcessSQSMessage(Amazon.SQS.Model.Message sqsMessage)
+ {
+ // Extract trace context from SQS message attributes
+ var carrier = new Dictionary();
+
+ if (sqsMessage.MessageAttributes != null)
+ {
+ if (sqsMessage.MessageAttributes.TryGetValue("traceparent", out var tp))
+ {
+ carrier["traceparent"] = tp.StringValue;
+ }
+ if (sqsMessage.MessageAttributes.TryGetValue("tracestate", out var ts))
+ {
+ carrier["tracestate"] = ts.StringValue;
+ }
+ }
+
+ var propagationContext = Propagator.Extract(
+ default,
+ carrier,
+ (c, key) => c.TryGetValue(key, out var value) ? new[] { value } : Array.Empty()
+ );
+
+ var links = new List();
+ if (propagationContext.ActivityContext != default)
+ {
+ links.Add(new ActivityLink(propagationContext.ActivityContext));
+ }
+
+ using var activity = ActivitySource.StartActivity(
+ "sqs_process",
+ ActivityKind.Consumer,
+ parentContext: default,
+ links: links
+ );
+
+ await HandleMessage(sqsMessage.Body);
+
+ activity?.SetTag("messaging.system", "AmazonSQS");
+ }
+ }
+ ```
+
+
+
+### Best practices for span links [#span-links-best-practices]
+
+When implementing span links, follow these best practices:
+
+1. **Always propagate trace context**: Ensure W3C trace context (`traceparent` and `tracestate` headers) are included in message headers or metadata.
+
+2. **Validate span context**: Always check if the extracted span context is valid before creating a span link. Invalid contexts should not create links.
+
+3. **Use appropriate span kinds**: Set `PRODUCER` kind for message publishing spans and `CONSUMER` kind for message processing spans.
+
+4. **Add messaging attributes**: Include semantic conventions for messaging systems (like `messaging.system`, `messaging.destination`, `messaging.operation`) to provide context.
+
+5. **Consider sampling**: Both linked traces must be sampled to appear in New Relic. Adjust sampling strategies for critical workflows that use span links.
+
+6. **Handle batch processing carefully**: When processing batched messages, create individual span links for each message to maintain traceability.
-OpenTelemetry [span links](https://opentelemetry.io/docs/concepts/signals/traces/#span-links) are not currently supported by New Relic.
+### Viewing span links in New Relic [#viewing-span-links]
+
+Once you've implemented span links in your instrumentation, you can view and navigate them in the New Relic UI:
+
+1. Navigate to the [trace details page](/docs/distributed-tracing/ui-data/trace-details/#span-links) for a trace
+2. Look for the span links badge in the filter bar showing the number of spans with links
+3. Select a span with links to see the **Span links** tab in the span details pane
+4. Click on linked traces to navigate between related traces
+
+For detailed information on using span links in the UI, see [Understanding span links](/docs/distributed-tracing/ui-data/trace-details/#span-links).
diff --git a/static/images/distributed-tracing-span-links-empty-state.webp b/static/images/distributed-tracing-span-links-empty-state.webp
new file mode 100644
index 00000000000..141e7628e3b
Binary files /dev/null and b/static/images/distributed-tracing-span-links-empty-state.webp differ
diff --git a/static/images/distributed-tracing-span-links-filter-badge.webp b/static/images/distributed-tracing-span-links-filter-badge.webp
new file mode 100644
index 00000000000..1f5167e8197
Binary files /dev/null and b/static/images/distributed-tracing-span-links-filter-badge.webp differ
diff --git a/static/images/distributed-tracing-span-links-multiple-forwards.webp b/static/images/distributed-tracing-span-links-multiple-forwards.webp
new file mode 100644
index 00000000000..4cf05bc170e
Binary files /dev/null and b/static/images/distributed-tracing-span-links-multiple-forwards.webp differ
diff --git a/static/images/distributed-tracing-span-links-tab-detail.webp b/static/images/distributed-tracing-span-links-tab-detail.webp
new file mode 100644
index 00000000000..cdea0c5af7f
Binary files /dev/null and b/static/images/distributed-tracing-span-links-tab-detail.webp differ