Skip to content
Published on

NATS Server 2.14 — Fast Batch Publish, Built-In Server Scheduling, and the Homework Due Before 2.15

Share
Authors

Introduction — Why 2.14 Comes Right After 2.12

NATS Server 2.14.0 was released on April 30, 2026. The previous minor release, 2.12.0, shipped on September 22, 2025, so this is roughly 7 months later. The NATS team said in the 2.12 release post that they had switched to a 6-month release cadence, but the 2.14 release post attributes this cycle's roughly one-month delay to "increased AI security review and disclosure response activity." That AI-generated security reports are now pushing back an open-source maintainer's release schedule is exactly the kind of 2026 scenery you pick up reading between the lines of a release note.

Then there is the version number. The first line of the release notes states plainly that "the 2.13.x version was skipped." No reason is given — not in the release notes, not on the blog, not in the upgrade guide. The git history shows main jumped straight to v2.14.0-dev on September 23, 2025, the day after the 2.12.0 release, so this was not a renumbering after the fact — the skip was planned from the start of the cycle. As I will cover later, you can observe that this brings the JetStream API level number back in sync with the server's minor version, but that is my own inference, not an official explanation.

This post is not a highlight reel — it checks the release notes, the upgrade guide, the design documents (ADRs), and the server source directly to lay out what each feature actually does, does not do, and what operators need to watch for. If NATS itself is new to you, it is worth reading Distributed Messaging 2026 — Kafka 3.9 / NATS / Redpanda / Pulsar / RabbitMQ 4 / WarpStream Deep-Dive first.

Fast Batch Publish — The Successor to Async Publish

2.14's headline feature. Starting with the background: the atomic batch publish that 2.12 introduced staged N messages and committed the whole batch only if every consistency check passed — an all-or-nothing approach. That is exactly right for something like atomically updating the 5 keys that make up an address in a KV store, but staging imposes a practical limit on batch size (ADR-50 cites a 1,000-message limit for atomic batches).

2.14's fast batch serves a different purpose. The goal is not atomicity — it is throughput and flow control.

                Atomic Batch (2.12)         Fast Batch (2.14)
Purpose         all-or-nothing correctness  high-speed ingest + flow control
Storage         stage, then commit at once  store immediately as it arrives
Consistency chk whole-batch unit            per-message, inline
Size limit      yes (1,000 per ADR)         effectively unbounded
On failure      whole batch rejected        gap:fail — abort batch / gap:ok — report gap, continue
Leader change   batch abandoned             survives in gap:ok mode

The design's core is a control channel kept alive while the batch is open. The client does not wait for an ack per message; instead the server sends a flow-control ack (BatchFlowAck) roughly once every 10 messages. The server sets the flow rate — per the ADR, even if the client requests maximum flow, the server starts low (e.g., 1) and roughly doubles it when there is headroom, halving it when it detects load. The basis for that judgment is server-internal metrics such as the number of concurrent fast publishers, average message size, and the amount of in-flight data not yet persisted.

Gap (loss) handling is a choice between two modes. fail mode aborts the batch the instant a gap is detected, fitting cases that need ordering guarantees (e.g., file chunks in an object store); ok mode just reports the gap via a BatchFlowGap message and keeps going, fitting high-frequency metric publishers that can tolerate some loss. The ADR states this design's ultimate goal plainly — to replace existing clients' async publish with this.

On the performance claim, honesty is warranted. The official blog writes that throughput "can reach nearly 2x that of async publish depending on configuration," but this is a vendor's self-reported figure, and the measurement conditions (message size, replication level, hardware, number of clients) are not disclosed. No reproducible benchmark numbers are currently public. Still, the qualitative motivation holds up — unlike the existing async publish, which was prone to breaking under the lack of flow control when two object-store uploads land at once, the blog's claim is that because the server directly coordinates publishers' rates, even hundreds of concurrent publishers each get their own flow control (this, too, is a self-reported claim).

Client support should also be viewed realistically. Using this feature requires turning on the AllowBatchPublish option on a stream, plus a client that supports it, and as of this writing the official implementation is led by the Go extension library orbit.go's jetstreamext v0.3.0 (2026-04-21, FastPublisher). Two weeks later, v0.3.1 fixed stall, timeout, and reply-prefix bugs in FastPublisher. Core nats.go only got the stream config field in v1.52.0. In other words, this is a young feature whose first implementation went through bug fixes right after release. It is worth validating against your own workload before putting it on a production critical path.

Recurring Schedules and Sampling — Cron Moves Inside the Server

2.12 introduced "a message delivered once, later" (a delayed message) via the Nats-Schedule-TTL header. 2.14 extends this to recurring schedules. Attach an interval like Nats-Schedule: @every 5m, @hourly, or a cron expression to a message, and the server will keep producing messages to the target subject on that schedule.

nats pub -J 'schedules.orders.hourly' \
  -H "Nats-Schedule: @hourly" \
  -H "Nats-Schedule-TTL: 5m" \
  -H "Nats-Schedule-Target: orders" \
  'body'

The point is that you no longer need a dedicated publisher running a timer on the client side. Two derived features shipped alongside it. Subject sampling uses the Nats-Schedule-Source header to "read the last message on the source subject each time the schedule fires, and publish it to the target" — the flagship use case is server-side downsampling, e.g., pulling per-second sensor readings at the edge up to the cloud only once every 5 minutes (if there is no message on the source, the schedule message's own body is published as a fallback). Schedule rollup uses the Nats-Schedule-Rollup header to roll up that subject at publish time.

Constraints from ADR-51 that you will hit if you do not read the docs:

  • Each schedule needs a unique subject. To have multiple delayed messages going to the same target, make the schedule message's subject unique — e.g., orders.schedule.UUID — and collect them with Nats-Schedule-Target.
  • The minimum interval for @every is 1 second. Anything shorter is rejected.
  • Cron uses the 6-field form that includes a seconds field, and the default time zone is UTC. The Nats-Schedule-Time-Zone header accepts an IANA time zone (e.g., Asia/Seoul) — since the server resolves the name using the host's tzdata, this comes with an operational requirement that every server that will evaluate cron schedules has tzdata installed and current. Fixed offsets (+02:00) are not accepted.
  • The ADR advises against cron schedules that land on a DST transition — it states explicitly that a run can be skipped when the clock jumps forward, and can fire twice when the clock falls back.
  • If the server has been down for a long time and then recovers, schedules that already passed while it was down fire immediately. If you do not want a message scheduled a month ago to arrive right after recovery, the ADR's recommended defense is to attach Nats-TTL to the schedule message so it expires.
  • The ADR states that the Discard New policy is not supported on streams using schedules, and 2.14.2 tightened configuration-constraint validation for counter streams and schedules.

When not to use it — this is not a replacement for a distributed cron scheduler. Responsibilities like execution-history tracking, failure retry policy, and overlap prevention still belong on the consumer side; all the server does is "create the message on that schedule and put it in the stream." Conversely, it is a good fit for eliminating "a sidecar that just keeps a connection open to run a timer."

WorkQueue/Interest Sourcing No Longer Drops Data

A substantive consistency fix behind an unassuming name. Before 2.14, stream mirrors/sources did asynchronous replication internally via ephemeral consumers. The problem surfaces when the sourcing target is a WorkQueue- or Interest-retention-policy stream — for example, if a leaf node sourcing an Interest stream goes down for a week, interest is treated as having disappeared in the meantime, the data gets auto-deleted, and even after the leaf recovers, that gap is never replicated.

2.14 automatically switches to durable consumers when sourcing/mirroring WorkQueue and Interest streams. A new ack policy, AckFlowControl, acknowledges after persistence based on flow control; this consumer shows up in the consumer list, carries source domain/account/stream metadata, and keeps its state even across disconnects. For security-sensitive setups, bring-your-own-consumer is also supported — pre-create the consumer yourself so a counterparty can never touch its config.

One thing to watch during upgrades — in a mixed-version cluster, an upgraded server may try to create the new sourcing consumer while an older server logs a json: unknown field "sourcing" warning. Per the guide, it automatically retries the old-style approach, so this is a transient log that disappears once the whole upgrade is done. Downgrading hurts more — dropping back to 2.12 marks AckFlowControl consumers offline, unusable until you go back to 2.14.

The Consumer Reset API

There comes a moment in operations when you want to "run this consumer again starting from sequence N." Until now, the only way was to delete the consumer and recreate it with a starting sequence, which also shook loose everything tied to its name, subscribers, and ACLs. 2.14 adds the $JS.API.CONSUMER.RESET.stream.consumer API, which lets you reset delivery state to the ack floor or an arbitrary sequence without deleting the consumer. The guide defines the post-reset state as "equivalent to deleting and recreating from that sequence."

Three Things That Actually Matter From an Operator's Perspective

This section matters more for the upgrade decision than the feature list. All three are behavior changes explicitly documented in the upgrade guide.

First, filestore I/O errors now freeze streams. Previous versions did not handle some filesystem I/O errors properly, and the stream and server kept running regardless. Starting with 2.14, errors surface — the affected stream freezes and stops making progress, logs the error, and health checks report unhealthy. Other streams on the same server are unaffected, and for a replicated stream another replica transparently takes over. That said, the frozen server itself needs a restart to recover. Clearly better than rotting silently, but it is a new operational condition to monitor — add write errors in healthz failure messages to your watch list.

Second, Raft overload protection. Previously, if entries piled up in the Raft log faster than they could be committed and applied, memory and disk could grow without bound. 2.14 recognizes this state, bounds resource usage, and has a lagging leader step down on its own, handing off to a healthier peer. The guide is explicit about the limits too — if a majority of peers are equally overloaded, the system stays in a degraded state; this protection is a safety net for transient overload, not a substitute for adequate capacity. The fact that this sentence appears in the release documentation is itself a credit to this project's documentation.

Third, homework to do before 2.15 — migrating the ACK subject format. 2.14 starts supporting the v2 format (which includes a domain/account hash) for consumer ack/flow-control reply subjects, alongside v1.

v1: $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<ts>.<pending>
v2: $JS.ACK.<domain>.<acct hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<ts>.<pending>

2.14's default is still v1, but 2.15 switches the default to v2. The guide states plainly that if you use account import/export or subject permissions that embed the stream name in ACK/FC subjects, you need to update your ACLs before 2.15 (this does not apply if you use catch-all wildcards or run JetStream in a single account only). A feature_flags field was added to server config in 2.14 for advance testing, letting you turn on js_ack_fc_v2. One catch: if you downgrade to 2.12 with this field set, the older version will not recognize it, so you need to clear it before downgrading.

Beyond that, there are some quietly helpful changes: read-only requests like info/list queries now go into a queue separate from create/update/delete requests and get lower priority (easing situations where dashboard polling crowds out operational work), and replicated streams' state snapshots are now written asynchronously without pausing stream processing — reportedly especially effective for tail latency on streams with heavy interior deletes, though no concrete figures are public.

Feature Gating by API Level, and Three Release Trains

Since 2.11, NATS gates JetStream features by API level. Checked directly in the server source (server/jetstream_versioning.go): 2.11 is level 1, 2.12.0 is level 2, and 2.14.0 is level 4. So where did level 3 go? Interestingly, the maintenance train 2.12.5 (March 2026) uses level 3 — a minor API addition, like the window_size parameter for stream backups, landed in the patch train and consumed a level. As a result, skipping 2.13 means "minor version − 10 = API level" lines back up again at 2.14 — but again, there is no official statement that this is the reason for the skip.

What this level means in practice is downgrade safety. Streams/consumers using higher-level features get marked offline rather than corrupted on an older server (2.12's offline assets), and clients can state a required level via the Nats-Required-Api-Level header.

Release-train operations are worth watching too. Per the GitHub release history, 2.12.x keeps receiving patches even after 2.14.0 shipped, released side by side on the same day — 2.12.10 and 2.14.2 together on June 2, and 2.12.12 and 2.14.3 together on June 29. Meanwhile 2.11.x stopped at 2.11.17 on April 27, right before the 2.14.0 release. So the observed pattern is dual maintenance of "current + previous" trains, but I could not find an official support-window policy document, so this is a pattern read from history, not a guarantee.

So When Should You Upgrade, and When Should You Wait

Cases with a reason to upgrade right away — topologies sourcing WorkQueue/Interest streams from leaf nodes (the consistency fix is substantive), operational patterns that need consumer reset, environments where surfacing filestore I/O errors early is welcome (edges with unreliable storage), and IoT/edge deployments where built-in server scheduling/downsampling eliminates a sidecar.

Cases with no reason to rush — if fast batch is the draw, given that the client ecosystem is still centered on orbit.go (Go) and the first implementation just went through bug fixes, it is better not to put it on a core path without your own workload benchmark. Since the 2.12.x train is still getting patches on the same dates, staying on 2.12.12 is a defensible choice for now if you do not need the new features. Either way, though, if you are a multi-tenant environment with fine-grained ACK subject ACLs, it is right to start preparing your js_ack_fc_v2 migration now, before the default changes in 2.15.

On a related note, elsewhere in the messaging ecosystem at the same time, Kafka is redesigning its storage layer itself with KIP-1150 diskless topics. Kafka is pushing broker disks out to object storage, while NATS is having the server directly coordinate publishers' flow — it is interesting to watch these two approaches to solving the throughput-and-cost problem diverge side by side. The positioning difference between the two systems is covered in the Comprehensive Messaging Systems Guide.

Closing

To sum up, 2.14 is less a flashy release than one that nudges JetStream a notch toward bulk ingest and unattended operation. Fast batch solves async publish's structural weakness (no flow control) via server-led coordination, scheduling absorbs the timer sidecar into the server, and WorkQueue/Interest sourcing quietly plugs a hole that used to lose data. In exchange, operators get a new thing to watch (stream freeze) and a migration to do (ACK v2 ACLs).

The performance numbers are still vendor self-reported only, so base your adoption decision on measurements from your own workload, not on the documentation's claims. And this release's documents — an upgrade guide that states its own limits, an ADR that admits a schedule can fire twice across a DST transition — are a pretty good place to start that measurement.

References