Skip to main content

Release Notes

Use whenyou are upgrading and need to know what changed.

You getwhat changed in each release, with the breaking parts called out.

Notable changes per version. What you have to change when upgrading is in Upgrading, and the full commit-level record is in CHANGELOG.md.

v3.0​

The headline is multiple connections: one service can talk to more than one NATS cluster through a connections map, each with its own streams, consumers, routers and backpressure budget. Health gains a per-connection breakdown, shutdown drains every connection in parallel, and two connections that would resolve the same stream fail at startup.

Recurring schedules bring cron and interval jobs, declared on a handler or created from a client, fired by NATS 2.14 itself and kept alive across deploys and replicas. A client can also schedule a one-shot delivery far past the events stream's max_age.

Features an old NATS server would accept and silently ignore, such as per-message TTL on 2.10 or scheduleAt() on 2.11, now fail with a JetstreamCapabilityError instead.

Dead letters now also cover deliveries no handler settled: a message whose final attempt ended in a crash, a stopped pod or a handler outliving ack_wait reaches the DLQ instead of sitting in the stream unnoticed. Ack extension takes a per-message limit, and logs when a handler reaches it.

The package loads in native ESM apps on NestJS 10 and 11, and @opentelemetry/api is a required peer dependency, since the transport always loaded it.

Several defaults changed. Streams reserve far less storage, retries are paced instead of firing immediately, and the dead-letter stream is on unless you opt out. Node 22 is the new minimum. What to change, and how to keep the old behaviour, is in Upgrading.

Earlier releases​

Every 2.x release, newest first.

v2.12 → v2.13

New features

  • Bring your own infrastructure: provisioning: { management: ManagementMode.Manual } binds to streams and consumers provisioned by Terraform, ArgoCD, or a platform team. The transport validates what it finds and never creates or updates it.
  • Custom names are honored. stream.name, consumer.durable_name and subject prefixes set in a kind's overrides are now read and applied, instead of being silently ignored in favor of the naming conventions.

Behavior changes

  • If you had stream.name or consumer.durable_name set by accident, the transport now uses those names instead of the convention-derived ones. Review your overrides blocks before upgrading.

Bug fixes

  • Consumer filters that would swallow scheduled messages are rejected at boot instead of silently dropping them.
  • Unfiltered consumers are treated as covering every handler subject, so binding to them no longer fails validation.
  • Self-RPC, broadcast publishing and metric labels all resolve names through the same resolver, so custom names apply consistently.
v2.11 → v2.12

New features

  • Provisioning boot summary and errors you can act on. Startup logs what each stream reserves, and provisioning failures name the entity, the limits involved, and what to do about it. Opt into a storage preflight to catch an over-committed file store before it bites.

Bug fixes

This release closed a set of silent data-loss and reliability gaps. The ones worth knowing about:

  • Destructive stream migration no longer has data-loss windows, and services can no longer clobber the shared broadcast stream.
  • Routers subscribe before consumers start delivering, so a consumer flushing its backlog at startup no longer drops messages onto a subject nobody is watching yet.
  • Dead-letter handling engages when dlq is configured without onDeadLetter, unroutable messages are captured in the DLQ instead of being terminated, and ctx.retry() on the final delivery escalates to dead-letter handling instead of vanishing.
  • The DLQ publish is retried in-process, and the transport no longer promises redelivery it cannot make.
  • Ack extension keeps running for backlogged messages, and settlement failures on a degraded connection are contained instead of taking the pipeline down.
  • Scheduling fixes: per-message TTL applies to the delivered message instead of the schedule holder, each scheduled message goes to a unique subject, and scheduleAt is rejected for ordered patterns.
  • NATS control headers set by user code are blocked and stripped from DLQ republishes.
  • The DLQ stream retention changed to Limits, and empty payloads decode as undefined.
v2.10 → v2.11

New features

  • Prometheus metrics: built-in prom-client-based metrics covering throughput (jetstream_messages_received_total, jetstream_publish_total, jetstream_messages_dead_letter_total), latency histograms (jetstream_handler_duration_seconds, jetstream_publish_duration_seconds, jetstream_rpc_duration_seconds), and consumer lag gauges (jetstream_consumer_num_pending, jetstream_stream_messages, etc.). Enable via forRoot({ metrics: true }). Writes to prom-client's global register by default, so pairing with @willsoto/nestjs-prometheus is zero-config. Adds TransportEvent.HandlerCompleted, TransportEvent.Published, and TransportEvent.RpcCompleted to the public TransportHooks surface for users who want to subscribe directly.

Peer dependency (optional)

  • prom-client is now declared as an optional peer dependency (^15.0.0). Required only when metrics is enabled. The transport never imports it when metrics is omitted or false, so applications that do not use the metrics feature pay nothing: zero bundle weight and zero runtime cost.

Documentation reorganization

  • The Distributed Tracing guide moved from /docs/guides/distributed-tracing to /docs/observability/tracing. The new top-level Observability section in the sidebar groups both tracing and metrics under a single heading with a shared overview page. Bookmarks and external links to the old path should be updated.

No breaking API changes. Existing applications upgrade by bumping the dependency.

v2.9 → v2.10

New features

  • Distributed tracing with OpenTelemetry: every publish, consume, and RPC round-trip now produces an OpenTelemetry span. W3C Trace Context propagates through NATS message headers, so a single trace flows end-to-end across services. Activates automatically the moment your application registers an OpenTelemetry SDK (Sentry, Datadog, NodeSDK, etc.); zero runtime cost when no SDK is registered. Configurable through forRoot({ otel: ... }).
  • Header contract: formal documentation of the NATS message headers the transport reads and writes. Use this as the integration spec when publishing to (or consuming from) the transport from other languages (Go, Python, etc.).

Peer dependency (optional)

  • @opentelemetry/api is now declared as an optional peer dependency. Applications that already register an OpenTelemetry SDK (@sentry/node, @datadog/tracer, @opentelemetry/sdk-node, etc.) bring it in transitively; no action needed. Applications that want to use the library's distributed-tracing feature and do not already have an OTel SDK must install @opentelemetry/api at the same major version (^1.9.0). This avoids the silent "no-op tracer" trap where two copies of the API live in node_modules and the global tracer singleton refuses the mismatched version.

Behavior changes (non-breaking API, observable in logs/APM)

  • Reduced internal logging. The logger.error sites that duplicated existing TransportHooks events have been removed. The hook is now the single observability channel for those events. If you relied on those log lines for monitoring, register the relevant hook (error, rpcTimeout, deadLetter).
  • Error classification on OTel spans. When OpenTelemetry is enabled, handler throws of RpcException and HttpException produce OK spans with jetstream.rpc.reply.has_error and jetstream.rpc.reply.error.code attributes; they are treated as expected business outcomes per the RPC contract. Bare Error throws produce ERROR spans with recordException. This keeps APM error rates clean for known business denials while loud-failing on real bugs.
  • TransportEvent.Error now fires on every handler throw, both event and RPC paths. Previously only some paths emitted it. If you have an error hook registered, it will now receive these (which is what most users want).
  • RpcConfig.timeout in JetStream mode now bounds connect + RPC. Previously the JetStream-mode per-request deadline only started after await connect() resolved, which meant a permanent NATS outage could accumulate pending RPCs indefinitely. The deadline is now armed immediately so callers always see a timeout. Core-mode RPC still relies on nats.js's own nc.request({ timeout }), which starts after connect() resolves. Against Core mode, a permanent outage wants maxReconnectAttempts set, so the retry loop stops at the protocol layer.

No breaking API changes. Existing applications upgrade by bumping the dependency.

v2.8 → v2.9

Notable change

Broadcast max_age drops from 1 day to 1 hour

Broadcast messages stay relevant for minutes, not days, so an hour of catch-up covers a pod that just started at a fraction of the storage. This is a mutable property, existing streams update automatically on next application startup. If you need a longer retention window, override it explicitly:

broadcast: { stream: { max_age: toNanos(1, 'days') } }

New features

  • Built-in Dead Letter Queue stream: add the dlq: { stream } option to forRoot() and the library provisions a dedicated DLQ stream on startup. Exhausted messages are automatically republished with tracking headers (x-dead-letter-reason, x-original-subject, x-original-stream, x-failed-at, x-delivery-count). The onDeadLetter callback remains available as a standalone option or as a notification/safety-net hook when combined with the DLQ stream.
  • Per-message TTL: JetstreamRecordBuilder.ttl(duration) sets the Nats-TTL header (NATS 2.11+), allowing individual messages to expire independently of the stream's max_age. Requires allow_msg_ttl: true on the target stream.
  • Handler metadata registry (NATS KV): with the metadata option on, every registered @EventPattern and @MessagePattern handler is published to a NATS KV bucket at startup. Other services discover them there, with no separate service registry.
  • Stream migration: automatic blue-green stream recreation for immutable property changes (storage, retention, etc.). Enable with allowDestructiveMigration: true on a per-stream basis.
  • Consumer self-healing auto-recreation: consumers deleted externally (via NATS CLI, cluster issues) are automatically recreated on the next poll. Migration-aware: waits during active stream migrations.
  • StreamConfigOverrides type, which stops anyone overriding retention (transport-controlled).
  • NatsErrorCode enum for NATS JetStream API error codes, so error handling code can switch on typed constants instead of magic strings.

No breaking changes.

v2.7 → v2.8

Breaking change: migrated from nats package to @nats-io/* scoped packages (v3.x).

This is an internal change: the library re-exports everything users need. If you import types directly from nats in your own code, update them:

- import { JsMsg, NatsConnection } from 'nats';
+ import { JsMsg } from '@nats-io/jetstream';
+ import { NatsConnection } from '@nats-io/transport-node';

New features

  • Message scheduling: one-shot delayed delivery via scheduleAt() (requires NATS >= 2.12)
  • allow_msg_schedules stream config option
v2.6 → v2.7

v2.7 added handler-controlled settlement, a way to ack, nak, or terminate a message without throwing.

New features

  • Handler-controlled settlement via ctx.retry() and ctx.terminate(): control message acknowledgment without throwing errors
  • Metadata getters on RpcContext: getDeliveryCount(), getStream(), getSequence(), getTimestamp(), getCallerName()

No breaking changes.

v2.5 → v2.6

v2.6 was the performance release, concurrency control, ack extension, and reworked defaults for reconnection and compression.

New features

  • Configurable concurrency for event/broadcast/RPC processing
  • Ack extension (ackExtension: true) for long-running handlers
  • Consume options passthrough for advanced prefetch tuning
  • Heartbeat monitoring with automatic consumer restart
  • S2 stream compression enabled by default (see Stream Defaults)
  • Performance connection defaults (unlimited reconnect, 1s interval)

No breaking changes.

v2.4 → v2.5

Breaking change: nanos() renamed to toNanos().

- import { nanos } from '@horizon-republic/nestjs-jetstream';
+ import { toNanos } from '@horizon-republic/nestjs-jetstream';

consumer: {
- ack_wait: nanos(30, 'seconds'),
+ ack_wait: toNanos(30, 'seconds'),
}
v2.3 → v2.4

New features

  • Ordered events (ordered: prefix, DeliverPolicy options)
  • Custom message IDs via setMessageId() for publish-side deduplication
  • Documentation site (Docusaurus)

No breaking changes.

v2.1 → v2.2

New features

  • Dead letter queue support via onDeadLetter callback
  • DeadLetterInfo interface with full message context

No breaking changes.

See also​