NATS JetStream as the AI Agent Event Bus: Patterns From a 6-Container Pod
By Sagar Shankaran, Founder of CallSphere
NATS 2.11 ships per-message TTLs, subject delete markers, and cluster_traffic isolation. We map every feature to AI-agent event flow and show how CallSphere wires it across a 6-container Real Estate pod.
Key takeaways
TL;DR — NATS JetStream 2.11 is the smallest event bus that can carry an AI agent stack: subject-based routing, per-message TTLs, exactly-once consumers, and a 15 MB binary. CallSphere's Real Estate OneRoof pod runs six containers communicating only over JetStream — no HTTP between agents, no Kafka cluster to babysit.
The pattern
A multi-agent AI system needs a backbone that routes intents, tool calls, and events across processes. Three options dominate in 2026: Kafka (heavy, log-centric), RabbitMQ (queue-centric, classic), and NATS JetStream (subject-centric, lightweight). For a sub-10-service AI pod where every microservice is a Python or Node process running an LLM agent, JetStream wins on operator cost: one binary, one config, one kubectl manifest.
NATS 2.11 (March 2025, current line through 2026) added the features that made it production-grade for AI workloads: per-message TTL via the Nats-TTL header, subject delete markers when MaxAge expires the last message on a subject, stream ingest rate limiting (max_buffered_msgs), and cluster_traffic isolation so Raft replication doesn't head-of-line-block customer streams.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for real estate in your browser — 60 seconds, no signup.
How it works (architecture)
flowchart LR
Caller[Realtime caller] --> Voice[Voice agent container]
Voice -->|publish call.intent.*| JS[(NATS JetStream<br/>subjects + streams)]
JS -->|consumer call.intent.book| Book[Booking agent]
JS -->|consumer call.intent.escalate| Esc[Escalation agent]
JS -->|consumer call.intent.research| Res[Research agent]
Book -->|publish tool.call.crm| JS
Res -->|publish tool.call.web| JS
JS -->|consumer tool.call.*| Tools[Tool executor]
Tools -->|publish tool.result.*| JS
JS -->|consumer call.audit| Audit[Audit + analytics]
Each container is a JetStream consumer on a specific subject filter. Messages are durable, ack'd, and replayed if a consumer dies. The voice agent never holds a TCP connection to a booking agent — it publishes call.intent.book with a correlation ID and walks away.
CallSphere implementation
Explore a live demo and compare current plans to find the right fit for your business.
Our Real Estate OneRoof deployment uses a NATS JetStream bus across a six-container pod (orchestrator, listing-search, scheduler, comp-pull, MLS-lookup, voicemail-handler). After-hours uses a Bull/Redis queue for delayed callback retries — different tool for different access patterns. Browse plans at /pricing or take a demo.
Build steps with code
- Install JetStream: deploy with
nats-server -js -m 8222in a 3-node cluster. - Create a stream:
nats stream add CALL_INTENT --subjects "call.intent.*" --storage file --replicas 3. - Add a TTL header on publish so stale intents auto-expire.
- Create a durable consumer per agent:
nats consumer add CALL_INTENT booking --filter call.intent.book --ack explicit. - Use queue groups for horizontal scaling (multiple booking agents share work).
- Wire dead-letter by setting
max_deliver=5and routing to aDLQ.call.intentstream. - Observe via
nats stream reportand Prometheus on port 7777.
import { connect, JSONCodec, AckPolicy } from "nats";
const nc = await connect({ servers: "nats://nats:4222" });
const js = nc.jetstream();
const sc = JSONCodec();
// Publisher (voice agent)
await js.publish(
"call.intent.book",
sc.encode({ callId: "abc", phone: "+18453884261", slot: "2026-05-08T15:00" }),
{ headers: { "Nats-TTL": "300" } } // 5-minute TTL
);
// Consumer (booking agent)
const c = await js.consumers.get("CALL_INTENT", "booking");
const messages = await c.consume({ max_messages: 10 });
for await (const m of messages) {
const intent = sc.decode(m.data);
await bookSlot(intent);
m.ack();
}
Common pitfalls
- No filter subject on consumer — you'll fan-in every message and starve other agents.
- Forgetting
ack explicit— at-most-once delivery silently drops on consumer crash. - One stream for everything — partition by domain (
CALL_INTENT,TOOL_CALL,AUDIT); MaxAge and replicas differ per domain. - Skipping cluster_traffic — under load, Raft replication shares the customer account and you see latency spikes.
- Treating JetStream like Kafka — it isn't a log warehouse; for 30-day analytics retention, fan out to S3 or ClickHouse.
FAQ
Why not Kafka? Six containers don't justify a 3-broker Kafka cluster + Schema Registry + Connect. JetStream's 15 MB binary handles everything you need for sub-100k msg/sec.
Still reading? Stop comparing — try CallSphere live.
See the real estate AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
How do we handle replay? nats consumer add ... --deliver all replays from stream start; --deliver by_start_time replays from a timestamp.
Is JetStream multi-tenant? Yes — accounts isolate streams, subjects, and JWT auth.
Can the voice agent block on a tool result? Use a request-reply with a temporary inbox, but for AI workloads we publish tool.call.* and listen on tool.result.{callId} instead.
Where does CallSphere put DLQ? A DLQ.* stream with a 7-day retention; a Slack bot pages on >0 depth.
Sources

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.