Skip to main content

Header Contract

Every NATS message header the transport touches — in one place. The contract is stable across minor versions; header names change only on major bumps. External publishers (Go, Python, Rust, …) only need to honour this page to interoperate with NestJS services using the library.

At a glance

HeaderReadWriteSourceWhat it does
traceparentW3C Trace ContextLinks the consume span to the upstream producer span.
tracestateW3C Trace ContextVendor-specific trace state. Forwarded as-is.
baggageW3C BaggageApp-level context propagation. Forwarded.
Nats-Msg-IdNATS standardDedup key. Surfaces on consume spans as messaging.message.id.
x-correlation-idRPCRPCLibraryIdentifies the matching RPC reply.
x-reply-toRPCRPCLibraryInbox subject for the RPC reply.
x-errorRPC replyRPC replyLibraryMarks the reply payload as an error envelope.
x-subjectLibraryOriginal subject the message was published to.
x-caller-nameLibraryInternal name of the sending service.
x-dead-letter-reasonDLQLibraryDLQ tracking — exhausted-retry reason.
x-original-subjectDLQLibraryDLQ tracking — original target subject.
x-original-streamDLQLibraryDLQ tracking — original stream name.
x-failed-atDLQLibraryDLQ tracking — ISO 8601 failure timestamp.
x-delivery-countDLQLibraryDLQ tracking — delivery attempt counter.

Header names are matched case-insensitively per the W3C Trace Context specification.

Reserved (you can't set these)

Calling JetstreamRecordBuilder.setHeader() with any of these throws a reserved-header error — they are populated by the library at publish time:

  • x-correlation-id · x-reply-to · x-error

The builder accepts values for these next two, but they're silently overwritten at publish time:

  • x-subject · x-caller-name

User-defined headers should use a distinct prefix or name (x-tenant-id, x-request-id, application-foo) and avoid the reserved names above.

NATS server-interpreted (Nats-* prefix)

These are interpreted by the NATS server itself, not by this library:

  • Nats-Msg-Id — publisher-supplied deduplication key. Set via JetstreamRecordBuilder.setMessageId() (from this library) or directly on the headers map (external publishers). Do not set it both ways on the same publish.
  • Nats-TTL, Nats-Schedule, Nats-Expected-*, Nats-Rollup, … — set them per the NATS docs when you need their semantics; otherwise leave them alone.

Cross-language examples

Publishing from Go (with OpenTelemetry)
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"github.com/nats-io/nats.go"
)

ctx, span := tracer.Start(ctx, "create-order")
defer span.End()

headers := nats.Header{}
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(headers))

js.PublishMsg(&nats.Msg{
Subject: "orders__microservice.ev.orders.created",
Data: payload,
Header: headers,
})

The NestJS consumer picks up traceparent from the headers and creates a CONSUMER span as a child of the Go producer span. The trace appears as a single end-to-end flow in your APM.

Publishing from Python (with OpenTelemetry)
from opentelemetry import propagate
from nats.aio.msg import Msg

headers = {}
propagate.inject(headers)

await js.publish(
subject="orders__microservice.ev.orders.created",
payload=payload,
headers=headers,
)
Reading traceparent manually (no OTel)

The header has the form:

00-<32-hex-trace-id>-<16-hex-parent-span-id>-<2-hex-flags>

Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01.

Per the W3C Trace Context specification, the version field is fixed at 00 (current) and the flags field's lowest bit indicates whether the trace is sampled. See the spec for the full grammar.

Compatibility

  • NATS server: >= 2.11 (preserves W3C Trace Context headers across publish and consume per ADR-41).
  • @nats-io/nats-core: inherited transitively via @nats-io/jetstream and @nats-io/transport-node (both pinned to ^3.3.1). You do not install nats-core directly — the resolved version is whatever those two pull in.
  • External publishers: any NATS client capable of attaching headers.

The library does not require a NestJS or TypeScript service on the other side of the wire. The header contract is the only coupling point.