ControlForge NATS Protocol Guide
James M. Belcher Founder, JMB Technical Services LLC May 2026 | ControlForge v1.0.814
1. Architecture Overview
ControlForge ships a complete NATS stack — both the client side and a fully-embedded broker — as native Structured Text functions. No sidecar daemon, no separate process, no add-on package. An ST program can publish to a remote NATS broker, receive subscriptions through a bounded ring buffer, do request/reply without blocking the scan loop, persist messages with JetStream, store key-value state, run its own broker, and join other ControlForge nodes into a NATS cluster — all from the same .goplc project.
There are two sides to the implementation, mirroring the MQTT layout:
| Role | Functions | Use Case |
|---|---|---|
| Client | NATS_CLIENT_CREATE / NATS_PUBLISH / NATS_SUBSCRIBE / NATS_REQUEST_* | Connect to existing NATS infrastructure, publish telemetry, react to commands, do RPC |
| Broker | NATS_BROKER_CREATE / NATS_BROKER_START | Run a broker inside ControlForge for edge deployments and goplc-to-controlforge clustering |
| JetStream | NATS_JS_* | Durable streams with replay — like Kafka, but built into the broker |
| Key/Value | NATS_KV_* | Bucketed key-value store on top of JetStream |
System Diagram
NATS vs MQTT — Why Both?
NATS and MQTT solve the same primary problem (broker-mediated messaging) but differ in important ways. ControlForge supports both because they're not interchangeable:
| Aspect | NATS | MQTT |
|---|---|---|
| Address structure | Subjects with . separators (plant.line1.temp) | Topics with / (plant/line1/temp) |
| Wildcards | * single token, > rest | + single level, # rest |
| Request/reply | Native first-class primitive | Not built in (use response topics) |
| Queue groups | Native, multiple consumers split load | Shared subscriptions (5.0 only) |
| Durable persistence | JetStream (built in, replayable) | Retained messages (last value only) |
| Key/value | JetStream KV built in | Not built in |
| Clustering | Native peer routes between brokers | Bridges only |
| Wire weight | Faster (text protocol, no per-message broker storage by default) | Smaller for QoS 0 |
| Use the embedded broker for | goplc-to-controlforge mesh, edge isolation | Local IIoT device hub |
If you're sharing data with HMI tools, sensors, MQTT-only field devices, or cloud IoT endpoints, use MQTT. If you're connecting ControlForge nodes to each other, doing RPC between programs, or want durable streaming with replay, use NATS.
NATS Concepts Quick Reference
| Concept | Description |
|---|---|
| Subject | Routing key (plant.line1.temp). Hierarchical, no pre-registration. |
| Wildcards | * = one token (plant.*.temp), > = rest (plant.>). |
| Publish | Fire-and-forget message to a subject. |
| Subscribe | Register interest; messages land in a per-subject bounded ring buffer. |
| Queue Group | Load-balanced subscription — N members in the group, only one gets each message. |
| Request/Reply | Built-in: requester gets a temporary inbox subject, listens for one reply. ControlForge implements this asynchronously — see §6. |
| JetStream | Durable persistence layer. Streams capture published messages; consumers replay. |
| KV Bucket | Key/value store on top of JetStream. Get / Put / Delete / Watch. |
| Cluster | Two or more NATS brokers connected by routes. Subscription interest propagates across the mesh. |
| NoResponders | NATS 2.x default: a request to a subject with zero subscribers returns immediately with an error rather than timing out. ControlForge surfaces this as ReqError (-2). |
2. Quick Start
The simplest possible NATS pipeline: one ST program, one external nats-server, one publish, one subscribe.
Step 1 — Run a NATS server
Anywhere on your network:
nats-server -js
(JetStream isn't needed for plain pub/sub but it's harmless to enable.)
Step 2 — Write an ST program
PROGRAM Main
VAR
init_done : BOOL := FALSE;
received : STRING := '';
END_VAR
(* First-scan setup *)
IF NOT init_done THEN
NATS_CLIENT_CREATE('nc', 'nats://localhost:4222', '');
NATS_CLIENT_CONNECT('nc');
NATS_SUBSCRIBE('nc', 'plant.line1.temp');
init_done := TRUE;
END_IF
(* Every scan: publish *)
NATS_PUBLISH('nc', 'plant.line1.temp', '23.4');
(* Every scan: drain one message if available *)
IF NATS_HAS_MESSAGE('nc', 'plant.line1.temp') THEN
received := NATS_NEXT_MESSAGE('nc', 'plant.line1.temp');
END_IF
END_PROGRAM
Run with the standard controlforge <project>.goplc --api-port 8082 and watch received update on every scan.
Step 3 — Watch from outside
nats sub --server nats://localhost:4222 "plant.>"
Every time the ST program publishes, the CLI prints the message — proving the ControlForge client is talking to a real broker, not a simulation.
3. Function Reference
Client lifecycle
NATS_CLIENT_CREATE(name, url, [client_id]) : BOOL
NATS_CLIENT_CREATE_AUTH(name, url, user, password) : BOOL
NATS_CLIENT_CREATE_TOKEN(name, url, token) : BOOL
NATS_CLIENT_CONNECT(name) : BOOL
NATS_CLIENT_DISCONNECT(name) : BOOL
NATS_CLIENT_IS_CONNECTED(name) : BOOL
NATS_CLIENT_DELETE(name) : BOOL
NATS_CLIENT_LIST() : STRING (CSV)
NATS_CLIENT_STATUS(name) : STRING
The first argument to every function is the local handle name you chose at create time. URL accepts comma-separated multi-server lists for high-availability: 'nats://primary:4222,nats://secondary:4222'.
Pub/Sub
NATS_PUBLISH(name, subject, payload) : BOOL
NATS_SUBSCRIBE(name, subject) : BOOL
NATS_QUEUE_SUBSCRIBE(name, subject, queue) : BOOL
NATS_UNSUBSCRIBE(name, subject) : BOOL
NATS_NEXT_MESSAGE(name, subject) : STRING (empty if none)
NATS_HAS_MESSAGE(name, subject) : BOOL
NATS_PENDING(name, subject) : DINT
NATS_DROPPED(name, subject) : DINT
Bounded ring buffer: Each subscription has a per-subject ring (default depth 1024). When the ring fills, the oldest message is dropped and NATS_DROPPED() increments. This is the right behavior for control loops — fresh data is more useful than ancient data — but if you're seeing drops you should either increase scan rate, drain in batches, or use JetStream pull consumers (§7) for at-least-once delivery.
Wildcards in subscribe: NATS_SUBSCRIBE('nc', 'plant.>') captures everything under plant.. Each delivered message includes its actual subject, available through NATS_NEXT_MESSAGE.
Async Request/Reply Triplet
NATS_REQUEST_SEND(name, subject, payload, [timeout_ms]) : DINT (request id, 0 on err)
NATS_REQUEST_RESULT(name, request_id) : INT
(0=pending, 1=ready, -1=timeout, -2=error/no-responders)
NATS_REQUEST_DATA(name, request_id) : STRING (drains handle)
NATS_REQUEST_DROP(name, request_id) : BOOL
This is the only correct way to do request/reply from ST. A naive synchronous call would block the scan loop while waiting for the responder; control logic would freeze. The triplet runs the wait in a background goroutine and lets your scan poll for the result.
VAR
req_id : DINT := 0;
reply : STRING := '';
state : INT := 0; (* 0=idle, 1=waiting, 2=done *)
END_VAR
CASE state OF
0: (* Send *)
req_id := NATS_REQUEST_SEND('nc', 'sensor.read', 'temp', 2000);
IF req_id > 0 THEN state := 1; END_IF
1: (* Poll *)
CASE NATS_REQUEST_RESULT('nc', req_id) OF
1: reply := NATS_REQUEST_DATA('nc', req_id);
state := 2;
-1, -2: state := 0; (* retry next cycle *)
END_CASE
2: (* Done — use reply, then back to idle *)
state := 0;
END_CASE
NATS_REQUEST_DATA auto-releases the handle on the first successful read; calling it again returns ''. NATS_REQUEST_DROP discards a pending request without reading.
JetStream (durable streams)
NATS_JS_PUBLISH(name, subject, payload) : BOOL
NATS_JS_PULL_SUBSCRIBE(name, stream, durable, [subject_filter]) : BOOL
NATS_JS_NEXT_MESSAGE(name, durable) : STRING
NATS_JS_PENDING(name, durable) : DINT
NATS_JS_DROPPED(name, durable) : DINT
NATS_JS_UNSUBSCRIBE(name, durable) : BOOL
JetStream gives you at-least-once delivery, replay, and rate-decoupling (publishers can fire faster than consumers). The stream and the durable consumer must be created out-of-band with the nats CLI or admin tooling — ST code only binds to existing infrastructure.
# Operator setup, once
nats stream add ALARMS --subjects "alarms.>" --storage memory --retention limits
nats consumer add ALARMS WORKER --filter "" --ack explicit --deliver all --pull
NATS_JS_PUBLISH('nc', 'alarms.high_temp', 'panel-3 over limit');
NATS_JS_PULL_SUBSCRIBE('nc', 'ALARMS', 'WORKER', 'alarms.>');
IF NATS_JS_PENDING('nc', 'WORKER') > 0 THEN
msg := NATS_JS_NEXT_MESSAGE('nc', 'WORKER');
END_IF
Pull-consume is the right model for ST: a background goroutine fetches in batches and fills a per-durable ring identical to the plain-subscribe ring. Messages are auto-acked on read.
Key/Value
NATS_KV_GET(name, bucket, key) : STRING (empty if missing)
NATS_KV_HAS(name, bucket, key) : BOOL
NATS_KV_PUT(name, bucket, key, value) : BOOL
NATS_KV_DELETE(name, bucket, key) : BOOL
NATS_KV_KEYS(name, bucket) : STRING (CSV)
Buckets are created out-of-band: nats kv add CONFIG. KV ops are short round-trips against the broker (sub-millisecond on a healthy LAN) so they're exposed as synchronous calls — no async triplet needed.
NATS_KV_HAS exists because NATS_KV_GET returns '' for both "key missing" and "key set to empty string." When the difference matters, check _HAS first.
Embedded Broker
NATS_BROKER_CREATE(name, port) : BOOL
NATS_BROKER_CREATE_AUTH(name, port, user, password) : BOOL
NATS_BROKER_CREATE_JS(name, port, store_dir) : BOOL
NATS_BROKER_START(name) : BOOL
NATS_BROKER_STOP(name) : BOOL
NATS_BROKER_DELETE(name) : BOOL
NATS_BROKER_IS_RUNNING(name) : BOOL
NATS_BROKER_URL(name) : STRING
NATS_BROKER_LIST() : STRING (CSV)
NATS_BROKER_NUM_CONNECTIONS(name) : DINT
NATS_BROKER_NUM_ROUTES(name) : DINT
NATS_BROKER_STATS(name) : STRING
CREATE only validates and constructs — call START to actually bind ports. Monitoring HTTP is disabled by default (set explicitly via Go-side BrokerConfig.MonitorPort if needed).
4. Worked Example: 11-Node Cluster with Chained Counter
This is the canonical demo for the embedded broker — and a real ControlForge project under projects/nats-cluster-demo/.
Topology:
Each minion appends ->N@<timestamp_ms> to the message it receives, so the boss's final string is a complete trace of the chain plus per-hop latency stamps.
Boss (node 0)
PROGRAM Boss
VAR
init_done : BOOL := FALSE;
delay_count : DINT := 0;
kicked : BOOL := FALSE;
final_value : STRING := '';
start_us : LINT := 0;
end_us : LINT := 0;
total_us : LINT := 0;
END_VAR
IF NOT init_done THEN
NATS_BROKER_CREATE('cluster_broker', 4226);
NATS_BROKER_START('cluster_broker');
NATS_CLIENT_CREATE('boss_client', 'nats://127.0.0.1:4226', '');
NATS_CLIENT_CONNECT('boss_client');
NATS_SUBSCRIBE('boss_client', 'chain.done');
init_done := TRUE;
END_IF
(* Wait ~3s for all minions to subscribe before kicking the chain *)
delay_count := delay_count + 1;
IF init_done AND NOT kicked AND delay_count > 60 THEN
start_us := NOW_US();
NATS_PUBLISH('boss_client', 'chain.1',
CONCAT('start@', LINT_TO_STRING(start_us)));
kicked := TRUE;
END_IF
IF NATS_HAS_MESSAGE('boss_client', 'chain.done') THEN
final_value := NATS_NEXT_MESSAGE('boss_client', 'chain.done');
end_us := NOW_US();
total_us := end_us - start_us;
END_IF
END_PROGRAM
Minion N (1..10)
PROGRAM MinionN
VAR
init_done : BOOL := FALSE;
last_in : STRING := '';
last_out : STRING := '';
END_VAR
IF NOT init_done THEN
NATS_CLIENT_CREATE('mN_client', 'nats://127.0.0.1:4226', '');
NATS_CLIENT_CONNECT('mN_client');
NATS_SUBSCRIBE('mN_client', 'chain.N');
init_done := TRUE;
END_IF
IF NATS_HAS_MESSAGE('mN_client', 'chain.N') THEN
last_in := NATS_NEXT_MESSAGE('mN_client', 'chain.N');
last_out := CONCAT(CONCAT(CONCAT(last_in, '->N@'),
LINT_TO_STRING(NOW_US())), '');
(* Last minion publishes to chain.done; others to chain.<N+1> *)
NATS_PUBLISH('mN_client', 'chain.<NEXT>', last_out);
END_IF
END_PROGRAM
Result
After all 11 nodes are running and the boss kicks the chain, its
final_value contains the full 11-stop trail with µs-precision
timestamps. Two live runs at different scan rates:
50 ms periodic scan (10 hops):
hop delta
1 +33.61 ms
2 +33.92 ms
3 +1.44 ms ← scan caught it immediately
4 +25.40 ms
5 +15.15 ms
6 +20.52 ms
7 +5.47 ms
8 +47.17 ms ← just missed a scan
9 +20.92 ms
10 +13.74 ms
─────────────────
10-hop total: 217.33 ms (mean 21.7 ms/hop)
boss.total_us: 250.18 ms (incl. return hop to chain.done)
5 ms periodic scan (same code, scan_time_ms=5):
hop delta
1 +1.57 ms
2 +1.54 ms
3 +14.45 ms ← jitter outlier (likely GC pause)
4 +3.11 ms
5 +3.25 ms
6 +6.24 ms
7 +1.40 ms
8 +6.58 ms
9 +3.79 ms
10 +4.46 ms
─────────────────
10-hop total: 46.39 ms (mean 4.64 ms/hop)
boss.total_us: 50.66 ms
1 ms periodic scan (same code, scan_time_ms=1):
hop delta
1 +0.28 ms ← lucky: scan caught it immediately
2 +1.27 ms ← unlucky: missed a scan
3 +0.17 ms ← lucky
4 +1.21 ms ← unlucky
5 +1.27 ms
6 +1.29 ms
7 +1.27 ms
8 +0.17 ms
9 +0.19 ms
10 +1.24 ms
─────────────────
10-hop total: 8.36 ms (mean 0.84 ms/hop)
boss.total_us: 9.67 ms (sub-10-ms 11-hop round trip)
Notice the bimodal distribution at 1 ms scan: every delta is either ~0.2 ms (the message arrived just before a scan) or ~1.2 ms (it just missed and had to wait a full period). This is exactly what "uniform-random 0–1 ms wait plus ~0.3 ms broker round-trip" predicts.
Scaling table:
| Requested scan | Actual achieved | 10-hop total | mean/hop | round-trip |
|---|---|---|---|---|
| 50 ms | 50 ms | 217.33 ms | 21.70 ms | 250.18 ms |
| 5 ms | 5 ms | 46.39 ms | 4.64 ms | 50.66 ms |
| 1 ms | 1 ms | 8.36 ms | 0.84 ms | 9.67 ms |
| 500 µs | ~1 ms | 5.49 ms | 0.55 ms | 6.40 ms |
Each 10× scan reduction yields ~5× chain reduction — the latency
floor is the ~0.2–0.3 ms broker round-trip plus ring-buffer hand-off,
which doesn't scale with scan period. Minion avg_scan_us stayed at
~30–55 µs across all four runs; the ST work itself is nowhere near
even the 500 µs budget.
The 500 µs run is interesting: we asked for 500 µs scan via
scan_time_us:500 in the task config, but the actual achieved scan
period is ~1 ms because Go's time.NewTicker on a stock Linux kernel
can't reliably fire faster than the kernel's HZ tick — the timer wakes
up on millisecond boundaries. Net result is a ~550 µs mean per-hop
latency, the bimodal "lucky" / "unlucky" pattern still holds.
If you need sub-500 µs reliable per-hop latency on a real PLC, the broker is no longer the bottleneck — it's the OS scheduler. Real-time options:
| Lever | Effect |
|---|---|
scan_time_us < 1000 | Half-helps: actual period clamped to ~1 ms by stock kernel HZ |
| PREEMPT_RT kernel | True µs precision; controlforge supports the preempt-rt snap plug |
SCHED_FIFO + CPU pinning | Removes the worst jitter spikes (GC pauses still bite) |
| Event-triggered task (wake on ring-buffer fill) | Eliminates the scan-phase wait entirely; runtime hooks are present, ST surface not yet wired |
The full .goplc project files for boss and all 10 minions are in projects/nats-cluster-demo/ — drop them into separate controlforge instances, point each at its assigned API port (8084 for boss, 8085-8094 for minions), and watch the chain run.
5. Cluster Mode (Multi-Broker Mesh)
When you want true broker-to-broker routing — multiple controlforge nodes each running their own broker, subscription interest auto-propagating between them — the embedded broker's cluster routing is the answer. This is configured at the Go level today (the ST broker functions don't expose cluster routes yet); see pkg/protocols/nats/broker_test.go TestBrokerCluster for the canonical Go API. A future revision of the ST surface will add NATS_BROKER_CLUSTER_*.
For pure ST-driven clustering, the pattern in §4 (one broker, many clients) gets you most of the way there. The dedicated cluster mode is for broker-to-broker redundancy and load distribution.
6. Authentication
Client side
NATS_CLIENT_CREATE_AUTH(name, url, user, password) — basic user/password (recommended for most deployments).
NATS_CLIENT_CREATE_TOKEN(name, url, token) — opaque bearer token (rotate via KV bucket, etc.).
Broker side
NATS_BROKER_CREATE_AUTH(name, port, user, password) enforces a single user. For multi-user setups with per-user pub/sub ACLs, use the Go-level BrokerConfig.Auth struct directly — see pkg/protocols/nats/broker.go. ST-level multi-user NATS_BROKER_ADD_USER is a future addition.
7. Performance
Ballpark numbers from the development laptop (Dell Precision 7740, Linux, Go 1.25):
| Operation | Latency |
|---|---|
| Embedded broker → ST client → embedded broker (loopback) | <0.5 ms |
| Single-broker chain across 10 ST programs | 1–5 ms total |
KV Put/Get on local broker | <1 ms |
| JetStream publish + ack on local broker | 2–10 ms |
Network latency dominates over LAN; local-loopback cluster numbers are essentially CPU-bound.
8. Comparison vs MQTT — When to Use Which
| Workload | Pick |
|---|---|
| Talk to MQTT-only field devices, sensors, HMI tools | MQTT |
| Cloud telemetry to AWS IoT / Azure IoT / Google IoT Core | MQTT |
| ControlForge-to-ControlForge mesh, edge clustering | NATS (embedded broker) |
| Synchronous-feeling RPC between programs | NATS (request/reply triplet) |
| Durable streaming + replay (alarm history, audit log) | NATS JetStream |
| Distributed key/value config without a separate DB | NATS KV |
| Lowest possible wire footprint for QoS 0 | MQTT |
| Subject ACLs and per-user pub/sub permissions | NATS (more flexible) |
You can run both simultaneously — they're independent stacks. A common pattern: NATS internally between controlforge nodes, MQTT externally to dashboards and devices.
9. Industrial Context — Where NATS Lives
Quick history. NATS was created by Derek Collison in 2010, originally in Ruby and rewritten in Go around 2012. It's been MIT-licensed from day one and is now a CNCF incubating project, with Synadia as the commercial steward. JetStream (durable streams) and accounts/multi-tenancy landed in NATS 2.0 (2019). So it's roughly the same vintage as MQTT 3.1.1 (the 2014 standard) and considerably older than Kafka was when Kafka first showed up in OT environments.
The "is NATS industrial?" question. It's the wrong shape of question. NATS isn't trying to compete with Sparkplug B for the device-to-SCADA tier. The protocols sit at different layers:
| Tier | Typical incumbent | Where NATS fits |
|---|---|---|
| Field bus (sensor → PLC) | Modbus, EtherNet/IP, PROFINET, CAN, BACnet | No — wrong physics; you need real-time deterministic I/O |
| PLC → SCADA / historian | Sparkplug B over MQTT, OPC UA | Possible, but Sparkplug B has the data model (UDTs, birth/death, host state) and the SCADA vendor support |
| Edge gateway → cloud | MQTT, NATS, Kafka, AMQP | Sweet spot. Higher fan-out than MQTT, lower per-broker storage cost than Kafka |
| Cross-plant / cross-site mesh | NATS, Kafka, RabbitMQ | Sweet spot. Native broker-to-broker routing makes multi-site trivial |
| OT/IT bridge microservices | NATS, gRPC, Kafka | Sweet spot. This is what NATS was actually designed for |
| Workload telemetry, fleet ops | NATS, Kafka | Sweet spot. Sub-ms request/reply makes RPC-style integration practical |
The honest summary: if a sensor talks to a PLC, NATS isn't in that conversation. If a PLC talks to a SCADA, Sparkplug B usually wins. But if a fleet of PLCs / edge gateways needs to talk to each other, to the cloud, or to plant-wide services — that's where NATS earns its keep.
Real industrial deployments using NATS:
| User | What they use it for |
|---|---|
| Tesla | Vehicle telemetry plane, factory data plane |
| Siemens MindSphere | Internal messaging fabric inside the IIoT cloud platform |
| General Electric | Predix and successor industrial cloud platforms |
| Bosch (the ctrlX vendor) | Several internal industrial-cloud platforms |
| Walmart | Store-to-cloud event mesh across 4,500+ stores |
| Ericsson 5G core | Inter-microservice messaging in network functions |
| Synadia customers | Process control, oil & gas, utilities, smart-city |
This is far from exhaustive — NATS is widely deployed; it's just quietly deployed inside platforms rather than as a marquee device-protocol the way Sparkplug B is.
The cultural gap. OT engineers usually haven't heard of NATS; IT/ cloud engineers usually haven't heard of Sparkplug B. Each side assumes its own messaging is what "real" systems use. ControlForge's value proposition in this conversation is that it speaks both fluently in the same runtime: Sparkplug B for the SCADA-facing path, NATS for the goplc-to-controlforge mesh and gateway-to-cloud path. That pairing is genuinely rare in the field today.
When to reach for NATS over Sparkplug B in goplc:
| Goal | Pick |
|---|---|
| Talk to Ignition, AVEVA, Wonderware, Inductive — any SCADA that consumes Sparkplug B | Sparkplug B |
| Multiple controlforge nodes need to share state (boss/minion, cell controllers, redundant pairs) | NATS |
| Send telemetry to a cloud event bus (Synadia NGS, self-hosted nats-server cluster) | NATS |
| Durable command queue with replay-from-offset semantics | NATS JetStream |
| Distributed key/value config without standing up etcd or Consul | NATS KV |
| Sub-millisecond request/reply between control programs | NATS (the async triplet — see §3) |
| Anything that an OT historian or HMI vendor will plug into | Sparkplug B |
The two stacks are independent and can run side-by-side in the same controlforge instance. The most common high-value pattern: NATS internally for mesh coordination and edge-to-cloud, Sparkplug B externally for SCADA integration. They don't fight.
10. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
NATS_CLIENT_CONNECT returns FALSE | Broker not reachable | Verify with nats sub --server <url> ">" from the same host |
NATS_REQUEST_RESULT returns -2 immediately | NATS 2.x no-responders shortcut: nobody is subscribed to that subject | Check the responder is up before requesting; consider a small startup delay |
| Messages dropped (NATS_DROPPED rising) | Ring buffer overflow (default 1024) | Drain faster, increase scan rate, or raise Config.InboxBufferDepth at create time (Go-side) |
NATS_BROKER_START returns FALSE | Port collision (4222 by default) — usually monitoring port 8222 | Pick a different broker port; embedded broker disables monitoring by default to avoid 8222 collision |
JetStream pull-subscribe returns FALSE | Stream or durable consumer doesn't exist | Create with nats stream add and nats consumer add first |
NATS_KV_GET returns '' for a key you just wrote | Either the key is genuinely empty, or it's missing | Use NATS_KV_HAS to disambiguate |
Diagnostic commands
# What's listening on the broker port?
nats server check connection --server nats://localhost:4222
# What's in this stream?
nats stream info <stream-name>
# What does a consumer's pending count look like?
nats consumer info <stream-name> <durable>
# All KV buckets
nats kv ls
11. Function Index
44 entries total — see /api/docs/functions?search=NATS for the live signature list.
Client (9): NATS_CLIENT_CREATE, NATS_CLIENT_CREATE_AUTH, NATS_CLIENT_CREATE_TOKEN, NATS_CLIENT_CONNECT (alias NATS_CONNECT), NATS_CLIENT_DISCONNECT (alias NATS_DISCONNECT), NATS_CLIENT_IS_CONNECTED (alias NATS_IS_CONNECTED), NATS_CLIENT_DELETE, NATS_CLIENT_LIST, NATS_CLIENT_STATUS
Pub/Sub (8): NATS_PUBLISH, NATS_SUBSCRIBE, NATS_QUEUE_SUBSCRIBE, NATS_UNSUBSCRIBE, NATS_NEXT_MESSAGE, NATS_HAS_MESSAGE, NATS_PENDING, NATS_DROPPED
Async req/reply (4): NATS_REQUEST_SEND, NATS_REQUEST_RESULT, NATS_REQUEST_DATA, NATS_REQUEST_DROP
JetStream (6): NATS_JS_PUBLISH, NATS_JS_PULL_SUBSCRIBE, NATS_JS_NEXT_MESSAGE, NATS_JS_PENDING, NATS_JS_DROPPED, NATS_JS_UNSUBSCRIBE
KV (5): NATS_KV_GET, NATS_KV_HAS, NATS_KV_PUT, NATS_KV_DELETE, NATS_KV_KEYS
Embedded broker (12): NATS_BROKER_CREATE, NATS_BROKER_CREATE_AUTH, NATS_BROKER_CREATE_JS, NATS_BROKER_START, NATS_BROKER_STOP, NATS_BROKER_DELETE, NATS_BROKER_IS_RUNNING, NATS_BROKER_URL, NATS_BROKER_LIST, NATS_BROKER_NUM_CONNECTIONS, NATS_BROKER_NUM_ROUTES, NATS_BROKER_STATS