mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

AI Agents

NATS for Agent Orchestration: Why Message Brokers Beat HTTP for Multi-Agent Coordination

How NATS pub-sub messaging solves agent-to-agent communication, state synchronization, and failure recovery better than REST APIs or direct RPC.

Source: nats.io
NATS for Agent Orchestration: Why Message Brokers Beat HTTP for Multi-Agent Coordination

Agent systems are becoming distributed systems. When you chain three agents together with HTTP calls, you get away with it. When you coordinate twenty agents across cloud boundaries, language runtimes, and ephemeral execution contexts, HTTP starts to hurt.

NATS published a protocol spec that treats agents as first-class messaging participants. The Synadia Agent Protocol for NATS defines discovery, conversation, and liveness primitives so agents can find each other, exchange typed streams, and handle failure without a central orchestrator knowing every agent’s implementation details.

This is not a framework. It is a contract. If your agent speaks NATS, any other agent on the same fabric can call it without caring whether it is a Python script, a hosted model wrapper, or a long-running service.

Why Message Brokers for Agent Coordination

HTTP works for request-response. It breaks down when you need:

  • Decoupled discovery: Agents come and go. A message broker lets agents announce capabilities without hardcoding endpoints.
  • Backpressure and flow control: Streaming responses from LLMs can overwhelm callers. NATS subjects support rate limiting and acknowledgment semantics.
  • Failure isolation: If an agent crashes mid-workflow, durable subscriptions and message replay let you recover without rebuilding the entire orchestration chain.
  • Multi-cloud and edge: NATS bridges cloud vendors, on-prem clusters, and edge devices with a single security model. HTTP requires API gateways, service meshes, or custom proxies.

When you orchestrate agents with HTTP, you rebuild these patterns in application code. When you use a message broker, they are transport primitives.

NATS Messaging Primitives for Agents

NATS supports three patterns that map directly to agent coordination needs:

PatternAgent Use CaseFailure Mode
Request-ReplyTool calls, synchronous queriesTimeout if agent crashes before reply
Pub-SubBroadcast events, state updatesSubscribers miss messages if offline
Streaming (JetStream)Durable state logs, replayStorage overhead, replay latency

Request-Reply for Tool Calls

An orchestrator sends a prompt to agents.summarize.request. Any agent subscribed to that subject receives the message, processes it, and replies. The orchestrator does not need to know which agent instance handled the request.

// Type definitions for request and response
public record PromptRequest
{
    public string Prompt { get; init; }
    public Dictionary<string, object> Context { get; init; }
}

public record AgentResponse
{
    public string Type { get; init; }  // "text", "tool_call", "error"
    public string Content { get; init; }
    public object Data { get; init; }
}

// Orchestrator sends a request
var response = await nats.RequestAsync<PromptRequest, AgentResponse>(
    "agents.summarize.request",
    new PromptRequest 
    { 
        Prompt = "Summarize Q3 earnings", 
        Context = new Dictionary<string, object> 
        { 
            ["document_id"] = "doc-123" 
        }
    }
);

If the agent crashes before replying, the orchestrator sees a timeout. You handle retries at the orchestration layer or use JetStream to persist the request for later replay.

Pub-Sub for State Broadcasts

When an agent updates shared state (a document index, a knowledge graph, a session context), it publishes to a subject. Other agents subscribe and update their local views.

// Agent publishes state update
await nats.PublishAsync("state.documents.updated", new DocumentUpdate 
{
    DocumentId = "doc-123",
    Embedding = embeddingVector
});

// Other agents subscribe
await foreach (var msg in nats.SubscribeAsync<DocumentUpdate>("state.documents.*")) 
{
    UpdateLocalIndex(msg.Data);
}

This decouples state synchronization from orchestration logic. Agents do not call each other directly. They react to events.

JetStream for Durable State Logs

JetStream adds persistence and replay. If an agent crashes, it can replay messages from the last acknowledged offset. If you need audit logs or debugging traces, JetStream stores the entire message history.

// Create a durable stream for agent interactions
var stream = await jsm.AddStreamAsync(new StreamConfig 
{
    Name = "AGENT_INTERACTIONS",
    Subjects = new[] { "agents.*.request", "agents.*.response" }
});

// Consumer replays from last acknowledged message
var consumer = await jsm.AddOrUpdateConsumerAsync("AGENT_INTERACTIONS", new ConsumerConfig 
{
    Durable = "orchestrator-consumer",
    AckPolicy = AckPolicy.Explicit
});

You pay for storage and replay latency. Use JetStream when you need guaranteed delivery or forensic visibility. Use core NATS pub-sub when you need speed and can tolerate message loss.

Synadia Agent Protocol: Discovery, Conversation, Liveness

The protocol defines three responsibilities:

1. Discovery

Agents announce capabilities by publishing to agents.discovery. Callers subscribe to that subject and build a registry of available agents and their supported operations.

{
  "agent_id": "summarizer-v2",
  "capabilities": ["summarize", "extract_entities"],
  "subjects": {
    "summarize": "agents.summarizer-v2.summarize",
    "extract_entities": "agents.summarizer-v2.extract"
  }
}

This avoids hardcoding agent endpoints. When you deploy a new agent version, it announces itself. Old versions stop announcing when they shut down.

2. Conversation

Agents receive prompts and return typed streams. The protocol specifies a JSON schema for requests and responses. Callers send a PromptRequest with context and tool definitions. Agents reply with a stream of AgentResponse messages (text chunks, tool calls, errors).

{
  "prompt": "Summarize Q3 earnings",
  "context": {
    "document_id": "doc-123"
  },
  "tools": [
    {
      "name": "fetch_document",
      "parameters": {
        "id": "string"
      }
    }
  ]
}

The agent streams back:

{ "type": "text", "content": "Q3 revenue increased by 15%..." }
{ "type": "tool_call", "tool": "fetch_document", "args": { "id": "doc-456" } }
{ "type": "done" }

This maps to OpenAI’s streaming API shape but runs over NATS instead of HTTP.

3. Liveness

Agents publish heartbeats to agents.heartbeat.<agent_id>. Orchestrators subscribe and track which agents are alive. If an agent stops sending heartbeats, the orchestrator removes it from the registry.

// Agent sends heartbeat every 5 seconds (C# 11+ PeriodicTimer)
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync()) 
{
    await nats.PublishAsync($"agents.heartbeat.{agentId}", new Heartbeat 
    {
        Timestamp = DateTimeOffset.UtcNow,
        Status = "healthy"
    });
}

This is simpler than HTTP health checks. No load balancer configuration, no polling endpoints. Agents broadcast liveness. Orchestrators listen.

Architecture: NATS Topology for Multi-Agent Systems

A typical NATS-based agent system has three layers:

  1. Core NATS cluster: Handles pub-sub and request-reply. Runs in Kubernetes, on-prem, or as a managed service. Agents connect via TCP or WebSocket.
  2. JetStream persistence: Stores durable streams for audit logs, state replay, and guaranteed delivery. Optional. Add it when you need durability.
  3. Agent mesh: Agents run anywhere (cloud functions, containers, edge devices). They connect to the NATS cluster and subscribe to subjects based on their capabilities.
Orchestrator (subscribes to agents.*.response)
                    |
          +---------v---------+
          |  NATS Cluster     |
          |  (pub-sub core)   |
          +---------+---------+
                    |
      +-------------+-------------+
      |             |             |
+-----v-----+ +-----v-----+ +-----v-----+
| Agent A   | | Agent B   | | Agent C   |
| (Python)  | | (C#)      | | (Rust)    |
+-----------+ +-----------+ +-----------+

Security boundaries map to NATS accounts. You can isolate agents by tenant, environment, or trust level without changing application code.

Failure Modes and Observability

Failure Modes

FailureImpactMitigation
Agent crashes mid-requestOrchestrator sees timeoutUse JetStream for durable requests, replay on restart
NATS cluster partitionAgents in different partitions cannot communicateDeploy NATS with clustering and failover, monitor split-brain
Message loss (core NATS)Subscribers miss updatesUse JetStream for critical messages, accept loss for ephemeral events
Slow consumerAgent cannot keep up with message rateNATS applies backpressure, drops messages, or disconnects slow consumers

Observability

NATS exposes metrics via Prometheus. Track:

  • Message rate per subject: Identifies hot agents or bottlenecks.
  • Consumer lag (JetStream): Shows how far behind an agent is in processing durable streams.
  • Connection count: Monitors agent churn and liveness.

You can also subscribe to agents.* with a monitoring agent that logs all interactions for debugging or compliance.

When to Use NATS for Agent Orchestration

Use NATS when:

  • You coordinate agents across cloud boundaries, languages, or runtimes.
  • You need decoupled discovery and do not want to hardcode agent endpoints.
  • You want built-in backpressure, retries, and failure isolation without rebuilding them in application code.
  • You need audit logs or replay for compliance or debugging.

Avoid NATS when:

  • You have a single-language, single-cloud system where HTTP is simpler.
  • You do not need decoupling and can tolerate tight coupling between orchestrator and agents.
  • You cannot run a message broker (serverless environments with no persistent connections).

Technical Verdict

NATS gives you the transport primitives that HTTP forces you to rebuild. If your agent system spans multiple clouds, languages, or execution contexts, a message broker is the right abstraction. The Synadia Agent Protocol maps agent coordination patterns (discovery, streaming responses, liveness) to NATS subjects and message types.

The trade-off is operational complexity. You run a NATS cluster. You monitor message rates, consumer lag, and connection health. You design subject hierarchies and security boundaries.

If you are already running microservices with NATS, adding agents is straightforward. If you are starting from scratch, evaluate whether the decoupling and failure isolation justify the infrastructure overhead. For systems with fewer than ten agents in a single cloud, HTTP may be simpler. For systems with dozens of agents across boundaries, NATS is the right tool.

Tags

agentic-ai orchestration infrastructure

Primary Source

nats.io ↗