Skip to main content

Header Contract

Use whenanother language or service reads or writes these messages.

You getevery header the transport touches, who sets it, and the stability guarantee.

The transport reads and writes the headers below and nothing else. Names are stable across minor versions and change only on major bumps, so a publisher in Go, Python or Rust interoperates with a NestJS service by honouring them.

At a glance​

HeaderReadWriteSourceWhat it does
traceparent✓✓W3C Trace ContextLinks the consume span to the upstream producer span.
tracestate✓✓W3C Trace ContextVendor-specific trace state. Forwarded as-is.
baggage✓✓W3C BaggageApp-level context propagation. Forwarded.
Nats-Msg-Id✓✓NATS 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-subject-✓LibraryOriginal subject the message was published to.
x-caller-name-✓LibraryInternal name of the sending service.
x-dead-letter-reason-DLQLibraryDLQ tracking: exhausted-retry reason.
x-original-subject-DLQLibraryDLQ tracking: original target subject.
x-original-stream-DLQLibraryDLQ tracking: original stream name.
x-failed-at-DLQLibraryDLQ tracking: ISO 8601 failure timestamp.
x-delivery-count-DLQLibraryDLQ tracking: delivery attempt counter.
x-schedule-hashHolderHolderLibraryFingerprint of a recurring schedule's definition. Skips unchanged rewrites.
x-schedule-patternHolderHolderLibraryHandler pattern a recurring schedule's ticks go to.
Nats-SchedulerTick-NATS serverSubject of the holder that fired the tick.
Nats-Schedule-Next--NATS serverNext fire time of the schedule, set on each tick.

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

A schedule holder is the message on the _cron. subject that defines a recurring schedule. The server copies the holder's headers, x-schedule-* included, into every tick it fires, so a tick handler sees them through ctx.getHeader() alongside Nats-Scheduler and Nats-Schedule-Next.

Reserved (you can't set these)​

JetstreamRecordBuilder.setHeader() throws on these, because the library populates them:

  • x-correlation-id · x-reply-to · x-error · x-schedule-hash · x-schedule-pattern

The builder accepts x-subject and x-caller-name. The transport writes both before your custom headers are applied, so a value you set for either one wins. Set them only when you are deliberately relabelling a message, because the consume span and ctx.getCallerName() read what arrives on the wire.

Give your own headers distinct names (x-tenant-id, x-request-id).

NATS server-interpreted (Nats-* prefix)​

setHeader() rejects every header starting with nats-, whatever the casing. These drive server-side behaviour: a stray Nats-Rollup purges every pending message on the subject. Reach them through the builder methods instead:

HeaderBuilder methodGuide
Nats-Msg-IdsetMessageId(id)Deduplication within duplicate_window
Nats-TTLttl(nanos)Per-message TTL
Nats-SchedulescheduleAt(date)Scheduling

External publishers in other languages set these directly on the headers map, as the NATS docs describe. Do not set Nats-Msg-Id both ways on the same publish.

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 reads traceparent and starts its CONSUMER span as a child of the Go producer span, so the trace shows as one 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.

The version is 00, and the lowest bit of the flags marks the trace as sampled. The W3C Trace Context specification has the full grammar.

Compatibility​

  • NATS server: headers work on every supported version. Nats-TTL and Nats-Schedule need newer servers, see NATS server versions.
  • External publishers: any NATS client that can attach headers. The other side of the wire does not need to be NestJS or TypeScript: these headers are the only coupling.