Skip to main content
When a request spans multiple services, you want one connected trace—not isolated fragments. This requires passing span context between services.

Context Propagation

Span context is a serialized representation of the current span. Pass it to downstream services, and they can continue the same trace. Common patterns:
  • HTTP headers — Include serialized context in a custom header (e.g., X-Laminar-Span-Context)
  • Message queues — Embed context in the message payload alongside your data
  • Database storage — Store context with workflow state for long-running processes that span multiple requests
The downstream service deserializes the context and uses it as the parent for its spans. The result: a single trace showing the full request flow across services.

Example

When you can’t pass span objects directly (HTTP, queues, cron jobs), serialize context in the upstream service and deserialize in the downstream service.
import { Laminar, observe } from '@lmnr-ai/lmnr';

// Service A
await observe({ name: 'serviceAHandler' }, async () => {
  const context = Laminar.serializeLaminarSpanContext();
  await fetch('https://service-b/api', {
    headers: { 'X-Laminar-Span-Context': context ?? '' },
  });
});

// Service B
const parentSpanContext = req.headers['x-laminar-span-context'];
const span = Laminar.startSpan({
  name: 'serviceBHandler',
  parentSpanContext: parentSpanContext as string | undefined,
});
span.end();
See also: Laminar.serializeLaminarSpanContext and Laminar.startSpan

Common Patterns

  • Database storage: store serialized context alongside workflow state, then reuse it when the workflow resumes.
  • Message queues: include context in the message payload so the consumer can continue the trace.

Database Storage Pattern

Persist span context with workflow state so you can resume a long-running trace later.
import { Laminar, observe } from '@lmnr-ai/lmnr';

// Start workflow and persist context
await observe({ name: 'workflow_start' }, async () => {
  const spanContext = Laminar.serializeLaminarSpanContext();
  await db.saveWorkflow({ userId, spanContext, data: workflowData, status: 'started' });
});

// Later: resume workflow and continue trace
const workflow = await db.getWorkflow(workflowId);
const span = Laminar.startSpan({
  name: 'workflow_continue',
  parentSpanContext: workflow.spanContext ?? undefined,
});
try {
  // Continue processing...
} finally {
  span.end();
}

Message Queue Pattern

Include span context in your queued message payload so consumers can continue the trace.
import { Laminar, observe } from '@lmnr-ai/lmnr';

// Producer
await observe({ name: 'task_enqueued' }, async () => {
  const spanContext = Laminar.serializeLaminarSpanContext();
  await queue.send({ data: taskData, spanContext });
});

// Consumer
const message = await queue.receive();
const span = Laminar.startSpan({
  name: 'task_processed',
  parentSpanContext: message.spanContext ?? undefined,
});
try {
  // Process the task...
} finally {
  span.end();
}

Notes

  • Within a service: prefer passing span objects and activating them (withSpan / use_span) instead of serializing context (see sdk/manual-spans).
  • When context is unavailable: if deserialization fails or context is missing, start a new trace rather than crashing.
  • Treat context as untrusted input: validate and fail open (see sdk/context-utilities).