Connection & lifecycle¶
Everything about the connection itself: how to configure it, how it reconnects when the network drops, how to observe its lifecycle, and how to shut it down cleanly.
ConnectOptions¶
Every connection setting lives on a single frozen, typed ConnectOptions
dataclass. You can pass its fields directly to connect as keyword arguments,
or build a ConnectOptions once and reuse it:
import natsio
# keyword form — the kwargs are a typed mirror of ConnectOptions
nc = await natsio.connect(
"nats://n1:4222", "nats://n2:4222",
name="my-service",
ping_interval=30.0,
)
# object form — build once, reuse
opts = natsio.ConnectOptions(
servers=("nats://n1:4222", "nats://n2:4222"),
name="my-service",
connect_timeout=5.0,
ping_interval=30.0,
)
nc = await natsio.connect(options=opts)
Passing both options= and keyword arguments is allowed — the keywords override
individual fields of the object. ConnectOptions also has a .replace(**changes)
helper that returns a modified copy.
Servers and connect¶
| Field | Default | What it does |
|---|---|---|
servers |
("nats://127.0.0.1:4222",) |
The server pool. Positional args to connect fill this in. |
name |
None |
Client name reported to the server (shows in monitoring). |
connect_timeout |
5.0 |
Per-server deadline for the initial handshake. |
no_randomize |
False |
Try servers in listed order instead of shuffling. |
ignore_discovered_servers |
False |
Ignore extra cluster URLs the server advertises. |
echo |
True |
When False, the server won't echo this client's own messages back to its matching subscriptions. |
inbox_prefix |
"_INBOX" |
Prefix for the request/reply reply inboxes. Must be a non-empty subject with no trailing dot. |
verbose / pedantic |
False |
Protocol-level +OK acks / strict subject checks (rarely needed). |
Liveness and limits¶
| Field | Default | What it does |
|---|---|---|
ping_interval |
120.0 |
Seconds between client PINGs that keep the link warm. |
max_outstanding_pings |
2 |
Unanswered PINGs before the connection is declared stale. |
max_payload |
(server-set) | Read-only via nc.max_payload — the server's cap, enforced client-side before publish. |
max_control_line |
4096 |
Max protocol control-line length. |
pending_msgs_limit |
65_536 |
Default per-subscription pending message limit. |
pending_bytes_limit |
64 MiB |
Default per-subscription pending byte limit. |
max_pending_size |
2 MiB |
Write-buffer high-water mark on the connected send path. |
request_timeout |
5.0 |
Default deadline for request / request_many. |
The pending limits set the defaults for new subscriptions; override them per
subscription with a PendingLimitPolicy — see
backpressure.
Reconnect¶
When the connection drops, natsio reconnects automatically and transparently: subscriptions are replayed, and publishes issued while disconnected are buffered and flushed on reconnect.
opts = natsio.ConnectOptions(
servers=("nats://n1:4222", "nats://n2:4222"),
allow_reconnect=True, # default; set False to connect exactly once
max_reconnect_attempts=60, # consecutive failures PER SERVER; -1 = forever
reconnect_time_wait=2.0, # base backoff between attempts
reconnect_time_wait_max=8.0, # backoff ceiling
reconnect_jitter=0.1, # random jitter added to each wait (plaintext)
reconnect_jitter_tls=1.0, # larger jitter for TLS reconnects
)
Buffering while disconnected¶
Publishes issued during a reconnect are buffered up to reconnect_buf_size and
replayed once the link is back:
- default (
8 MiB) — buffer up to 8 MiB of disconnected publishes; a publish past that raisesReconnectBufExceededError(non-fatal — reconnect continues, only that publish is rejected). 0— use the 8 MiB default.-1— disable buffering entirely; any publish while disconnected raisesReconnectBufExceededErrorimmediately.
Retry on the very first connect¶
By default, if the initial connect can't reach any server it raises. Set
retry_on_failed_connect=True to instead return a client already in the
RECONNECTING state — the first successful connect then fires Connected:
nc = await natsio.connect(
"nats://not-up-yet:4222",
retry_on_failed_connect=True,
max_reconnect_attempts=-1,
)
assert nc.status is natsio.ConnectionState.RECONNECTING # returned, not raised
The 2-strikes auth abort¶
If reconnect to a server fails with the same authentication error twice in a
row, natsio stops trying that path and finalizes the connection Closed —
retrying a genuinely-wrong credential forever is pointless. Set
ignore_auth_error_abort=True to keep retrying through repeated auth failures
instead.
Forcing a reconnect¶
force_reconnect() deliberately drops the current transport and reconnects
immediately — bypassing backoff, and not counting the drop as a server failure.
Subscriptions replay and buffered publishes survive; Disconnected then
Reconnected fire as usual. It is non-blocking: it returns once the drop is
scheduled, not once the link is back.
Lifecycle events¶
Two channels expose the connection's own state, independent of message traffic.
error_cb — a callback passed to connect, invoked for background errors
not tied to any single caller: a benign server -ERR, a slow-consumer report,
a crashed subscription callback. These are informational — the connection keeps
running.
async def on_error(err: natsio.NATSError) -> None:
log.warning("nats background error: %s", err)
nc = await natsio.connect("nats://localhost:4222", error_cb=on_error)
events() — an async stream of typed lifecycle events. Consume it in a
background task; the stream ends (returns) when the client is closed, so the
task exits on its own at shutdown.
async def watch(nc: natsio.Client) -> None:
async for event in nc.events():
match event:
case natsio.Connected(server_url=url) | natsio.Reconnected(server_url=url):
print("up:", url)
case natsio.Disconnected(error=err):
print("down:", err)
case natsio.LameDuck(server_url=url):
print("server going down:", url)
case natsio.ServersDiscovered(urls=urls):
print("cluster grew:", urls)
case natsio.ErrorOccurred(error=err):
print("background error:", err)
case natsio.Closed():
print("closed")
watcher = asyncio.create_task(watch(nc))
The event types are Connected, Disconnected, Reconnected, LameDuck,
ServersDiscovered, ErrorOccurred, and Closed. events() streams are
bounded and drop oldest on overflow, so a slow watcher can never stall the
connection.
Introspection¶
At any time you can read the connection's live state:
nc.status # a ConnectionState enum (CONNECTED, RECONNECTING, CLOSED, ...)
nc.is_connected # bool
nc.connected_url # the server currently attached to, or None
nc.server_info # the raw INFO dict from the server
nc.max_payload # server's max payload in bytes
ClientStatistics¶
nc.stats returns a point-in-time ClientStatistics snapshot of the client's
counters:
Drain vs close¶
Two teardown paths, and choosing correctly is the whole point:
await nc.close()— stop now. Pending writes are flushed, but anything still queued for a subscription is discarded, and an in-flightrequestraisesConnectionClosedError. Use it when you are aborting.await nc.drain()— stop cleanly. Unsubscribe everything (so no new messages arrive), let already-queued messages run through their handlers, then close. This is what a well-behaved worker does on shutdown. It is bounded bydrain_timeout(default 30s) — it will not hang forever, and the client is closed no matter what.
Both paths fire the final Closed event, which ends any events() stream.
Using the client as an async with context manager calls close() on exit;
call drain() explicitly when you need the graceful path.
Signals
For a service, wire loop.add_signal_handler(signal.SIGTERM, stop.set) and
await nc.drain() on the stop event so SIGINT/SIGTERM drains in-flight work
instead of dropping it. See examples/10_graceful_shutdown.py.
See also¶
- Core messaging — subscriptions and backpressure.
- Authentication & TLS — the auth-related options.
- Client API reference.