Skip to main content

natsio-jetstream-batch

JetStream fast-ingest batch publishing and batch reads for natsio, mirroring orbit.go/jetstreamext. Distribution natsio-jetstream-batch, imported as natsio.jetstream_batch. Zero runtime dependencies beyond natsio. Pre-1.0, no API-stability promises.

Requires nats-server 2.14+ for fast ingest (the allow_batched stream feature); batch direct get needs 2.11+ and allow_direct.

What it is

Fast ingest is the high-throughput half of JetStream batching. An atomic batch (allow_atomic) holds every message back until the commit; a fast batch (allow_batched) persists each message as it is added, and the commit only marks the end of the batch and returns the ack for the last stored message. The cost of that speed is that acks are periodic instead of per-message, so the client runs a small flow-control protocol:

  • the server acks every flow messages (it may ack more often, never less),
  • the publisher stalls once flow * max_outstanding_acks messages are unacked,
  • pings re-request the latest ack, so a lost ack costs a third of the ack deadline instead of the whole batch.

Batch reads (get_batch) pull many stored messages out of one Direct Get, starting from a sequence, a timestamp, or the lowest match. The complementary "last message on each subject" read already lives in core as Stream.get_last_msgs_for, and is not duplicated here.

Usage

import natsio
from natsio.jetstream import StreamConfig
from natsio.jetstream_batch import ALLOW_BATCHED, FlowControl, fast_publisher, get_batch

nc = await natsio.connect("nats://localhost")
js = nc.jetstream()

# Fast ingest needs allow_batched. Core's StreamConfig does not model the 2.14
# field yet, so it rides in `extra` (which round-trips untouched).
stream = await js.create_stream(
    StreamConfig(name="RAW", subjects=["raw.>"], allow_direct=True, extra={ALLOW_BATCHED: True})
)

# --- publish -------------------------------------------------------------
async with fast_publisher(js) as fp:
    for i in range(10_000):
        ack = await fp.add("raw.events", f"event {i}".encode())
        # ack.batch_sequence: this message's slot in the batch
        # ack.ack_sequence:   everything up to here is on disk (gap=fail mode)
    final = await fp.commit("raw.events", b"done")

print(final.stream, final.seq, final.size, final.batch_id)

# Leaving the block without committing sends an end-of-batch marker instead:
async with fast_publisher(js) as fp:
    await fp.add("raw.events", b"only this one")
# fp.ack is the batch ack; fp.is_closed is True

# Tuning and failure reporting. The handler runs on the batch's ack-reader
# task: keep it fast, and never call back into `fp` from it (that would wait
# for an ack only the reader can deliver — it raises `BatchReentrantUseError`;
# use `asyncio.create_task(fp.close())` if you must end the batch there).
def on_error(err: Exception) -> None:
    log.warning("fast batch: %s", err)

fp = fast_publisher(
    js,
    flow_control=FlowControl(flow=200, max_outstanding_acks=3, ack_timeout=10.0),
    continue_on_gap=True,      # tolerate holes instead of abandoning the batch
    error_handler=on_error,    # gaps, rejected messages, server-side abandon
)

# --- read ----------------------------------------------------------------
async for stored in get_batch(js, stream, 100, seq=1, subject="raw.events"):
    print(stored.seq, stored.subject, stored.payload)

async for stored in get_batch(js, "RAW", 10, start_time=an_hour_ago, max_bytes=64 * 1024):
    ...

API

Symbol Purpose
fast_publisher(js, *, flow_control=None, continue_on_gap=False, error_handler=None) Open a batch. No I/O, so await is optional.
FastPublisher.add(subject, payload, **expectations) -> FastPubAck Publish (and persist) one message; stalls under back-pressure.
FastPublisher.commit(subject, payload, **expectations) -> BatchAck Final message + end of batch. Valid as the first call.
FastPublisher.close() -> BatchAck | None End the batch without adding a message. Idempotent; None when nothing was added.
FastPublisher as async with commit yourself, or the exit close()s the batch (and abandons it if the block raised).
FastPublisher.is_closed / .batch_id / .size / .ack_sequence / .ack Introspection.
FlowControl(flow=100, max_outstanding_acks=2, ack_timeout=None) Ack cadence, back-pressure window, total ack deadline.
FastPubAck batch_sequence, ack_sequence.
BatchAck stream, seq, batch_id/batch, size/count, domain, val.
get_batch(js, stream, batch, *, seq=None, start_time=None, subject=None, max_bytes=None, timeout=None) Prepared batch read (BatchGet); iterate it, await optional.
parse_ack_frame / build_reply_prefix / build_reply_subject / build_message_headers The wire contract, exported for tooling and tests.

Per-message expectation keywords on add/commit: headers, ttl, expected_stream, expected_last_seq, expected_last_subject_seq, expected_last_subject_seq_subject — the same contract as core's JetStreamContext.publish.

Errors (all subclass natsio.jetstream.JetStreamError via JetStreamBatchError): BatchClosedError, BatchAbandonedError, BatchAckTimeoutError, ConcurrentBatchUseError, BatchReentrantUseError, BatchGapError, BatchMessageError, InvalidBatchAckError, BatchGetError, BatchGetUnsupportedError, BatchGetIncompleteError, plus the four server-reported APIError subclasses below.

Wire contract

Pinned by the unit tests against frames captured from nats-server 2.14.3, and cross-checked with orbit.go jetstreamext/fastpublish.go (buildReplySubject, ackMsgHandler, waitForStall) and getbatch.go.

A fast-ingest publish carries no batch headers at all. Everything the server needs is encoded in the reply subject:

<inbox>.<flow>.<gap>.<batch-seq>.<operation>.$FI
  • <inbox> — the ack inbox. Its last token is the batch id (the server slices it out and echoes it back as the ack's batch field); over 64 characters is rejected.
  • <flow> — the ack cadence asked for. Fixed at the initial value for the whole batch even after the server lowers it (orbit.go pins this too).
  • <gap> — fail (default: the server abandons the batch on a hole) or ok.
  • <operation> — 0 start, 1 add, 2 commit-with-message, 3 end-of-batch commit, 4 ping.

Frames arriving on the ack inbox:

Frame Meaning
{"type":"ack","seq":N,"msgs":M} Flow ack: persisted through batch sequence N, cadence is now M.
{"type":"gap","last_seq":N,"seq":M} Messages N..M-1 never arrived.
{"type":"err","seq":N,"error":{...}} Message N was rejected.
{"stream":S,"seq":N,"batch":B,"count":C} Terminal: the batch ack. count excludes the end-of-batch marker.
{"error":{...},"stream":S,"seq":0} Terminal: the batch failed.

A batch the server abandons (a rejected message, or a hole in fail mode) is terminated with a frame of the first shape — a normal-looking ack. The only tell is count: a batch we ended ourselves is counted whole (count equals every batch sequence we sent, one less for an end-of-batch marker, in both gap modes), an abandoned one stops short. Anything short raises BatchAbandonedError from the parked commit()/close() — data loss is never returned as an ack.

A batch read is $JS.API.DIRECT.GET.<stream> with {"seq"|"start_time", "next_by_subj", "batch", "max_bytes"}, answered by one message per hit (carrying Nats-Stream, Nats-Subject, Nats-Sequence, Nats-Time-Stamp, Nats-Num-Pending) and terminated by a 204 EOB status frame; a request that matches nothing gets a single 404.

Server error codes (a divergence worth knowing)

The four fast-ingest err_codes nats-server 2.14.3 actually emits are registered into core's registry on import, so they arrive typed:

Code Description Type
10205 batch publish is disabled FastBatchNotEnabledError
10206 batch publish pattern is invalid FastBatchInvalidPatternError
10207 batch publish ID is invalid FastBatchInvalidIdError
10208 batch publish ID unknown FastBatchUnknownIdError

orbit.go's jetstreamext/errors.go publishes these as 10203-10206, which cannot be right for 2.14 GA: nats.go's own registry already uses 10203 for JSErrCodeScheduleSourceInvalid and 10204 for JSErrCodeConsumerInvalidReset. We follow the server, not the constant.

Scope limits

  • Atomic batch publish is not implemented. PublishMsgBatch / BatchPublisher (the allow_atomic, Nats-Batch-Id / Nats-Batch-Sequence / Nats-Batch-Commit header protocol) is a different feature with different guarantees; this package is the fast-ingest half only.
  • GetLastMsgsFor is not reimplemented — core already has Stream.get_last_msgs_for (multi_last + 204 EOB).
  • One producer per batch. A FastPublisher is not safe for concurrent use (the oracle says so, and the sequence numbering makes it structural). Here the dangerous window is detected: a task that calls in while another is parked inside add/commit/close raises ConcurrentBatchUseError. Two calls that never suspend simply serialise — the guard is a diagnostic, not a lock.
  • The error_handler may not drive the publisher. It runs on the ack-reader task, so ending the batch from inside it would wait for an ack that only the blocked reader can deliver; that raises BatchReentrantUseError immediately instead of hanging until ack_timeout. Spawn a task if you need it.
  • close() on an untouched publisher returns None, where orbit.go returns ErrEmptyBatch. Nothing was opened, so there is nothing to end and nothing to fail — same reasoning as the empty batch read below. (After a failed or cancelled add, by contrast, close() re-raises the cause: a batch that persisted messages and then aborted is not an empty batch.)
  • Always close() (or use async with). A FastPublisher dropped without closing keeps its ack-reader task and ack subscription alive until the client itself closes — bounded, not a leak, but the server-side batch also stays inflight (no EOB) until it expires. The context-manager form frees both at block exit; prefer it.
  • A batch read that matches nothing yields nothing. orbit.go surfaces an ErrNoMessages through the iterator; an empty result is not an error here. A read that ends without the server's EOB is a different story and raises BatchGetIncompleteError.
  • ack_sequence under continue_on_gap=True guarantees nothing about the messages below it — that is the deal you make by tolerating gaps.
  • Nats-Msg-Id / Nats-Expected-Last-Msg-Id are not offered as per-message options (orbit.go documents them as unsupported inside a batch), though 2.14.3 does not reject a Nats-Msg-Id passed through headers=.
  • Passing a stream name to get_batch costs one STREAM.INFO round-trip to verify allow_direct; pass a Stream handle to skip it.
  • No client-side cap on batch length: fast ingest has no equivalent of the atomic batch's 1000-message server limit, and 20k-message batches are part of the local stress runs.

Example

A runnable script is at examples/basic.py — start a server with just server, then:

python extensions/natsio-jetstream-batch/examples/basic.py

Metadata

Release files for natsio-jetstream-batch 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for natsio-jetstream-batch 0.1.1
File Size Uploaded Provenance
natsio_jetstream_batch-0.1.1.tar.gz 24.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for natsio-jetstream-batch 0.1.1
File Interpreter ABI Platform
natsio_jetstream_batch-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 52.4 kB

Release files / natsio_jetstream_batch-0.1.1.tar.gz

Download URL natsio_jetstream_batch-0.1.1.tar.gz
Size 24.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b27695d088b9804676d2442140a4d02d2c536fd3035744cd40f463058800a54a
BLAKE2b-256 checksum
How to use checksums
dcadfff4cd5bbc64ba59784f5a1bf61086c7181b1d00d0f548af703a41f79f70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / natsio_jetstream_batch-0.1.1-py3-none-any.whl

Download URL natsio_jetstream_batch-0.1.1-py3-none-any.whl
Size 27.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8fb5a22394a9118e3b48126d4da00f46e3a4381b6ae776c209c30b8d18cbecd
BLAKE2b-256 checksum
How to use checksums
6a3b961fe85ce3766c5f84e0e122dc32c48c30868ec98466b60878fcb00a8bc4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for natsio-jetstream-batch 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for natsio_jetstream_batch-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page