# How It Works (/aiway/a2a/architecture)



The A2A connector is a **transparent JSON-RPC 2.0 proxy**. It never runs your agent's
logic — it routes a request to the right agent, waits for the reply, and relays it back
unchanged. The piece that makes agents plain HTTP servers (no KubeMQ SDK) is the
**virtual subscriber**: a per-agent internal bridge that turns a Query into an
outbound HTTP POST.

## Overview [#overview]

When a caller hits `POST /a2a/<agent_id>`, the request does **not** go straight to your
agent over HTTP. Instead it is published as a **Query** on the agent's internal
channel, picked up by that agent's virtual subscriber, and forwarded to the agent's
registered URL as an HTTP POST. The agent's response travels the same path in reverse.

This indirection is what lets agents stay simple — they are standard A2A-compliant HTTP
servers with **zero KubeMQ dependencies** (no broker client, no protobuf, no client library) — while
still benefiting from KubeMQ's routing, authorization, metrics, and persistence
infrastructure. For why agents are plain HTTP URLs, see the [overview](/aiway/a2a).

## The components [#the-components]

The gateway is one of several cooperating parts. The A2A connector handles the protocol;
the **Agent Registry** tracks who is registered; the **Subscriber Manager** owns the
lifecycle of every **virtual subscriber**; and the **Replicator** keeps registry state
in sync across a cluster.

<Mermaid
  chart="`
graph TB
CALLER[&#x22;Caller<br/>(any KubeMQ transport)&#x22;]
GW[&#x22;A2A connector<br/>JSON-RPC proxy&#x22;]
REG[&#x22;Agent Registry&#x22;]
STORE[&#x22;SQLite store&#x22;]
REPL[&#x22;Replicator&#x22;]
SUBMGR[&#x22;Subscriber Manager&#x22;]
VSUB[&#x22;Virtual Subscriber<br/>per agent&#x22;]
BROKER[&#x22;Message Broker&#x22;]
AGENT[&#x22;Agent<br/>HTTP server&#x22;]

CALLER --> GW
GW -->|Query<br/>_AGENTS_.agents/id| BROKER
GW --> REG
REG --> STORE
REG --> SUBMGR
SUBMGR --> VSUB
REPL -.->|_AGENTS_.discovery| BROKER
REG --> REPL
BROKER -->|deliver Query| VSUB
VSUB -.->|HTTP POST<br/>JSON-RPC 2.0| AGENT

class CALLER client
class GW,VSUB aiway
class REG,STORE,REPL,SUBMGR,BROKER broker
class AGENT external
`"
/>

*The A2A connector proxies via the broker; a per-agent virtual subscriber bridges the Query to an HTTP POST.*

| Component              | Role                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------ |
| **A2A connector**      | Transparent JSON-RPC 2.0 proxy; agent-management REST API; SSE relay                 |
| **Agent Registry**     | SQLite-backed store of registered agents with TTL liveness and cluster replication   |
| **Subscriber Manager** | Creates/destroys virtual subscribers on registration, deregistration, and TTL expiry |
| **Virtual Subscriber** | Per-agent broker-to-HTTP bridge; the reason agents need no KubeMQ SDK                |
| **Replicator**         | Propagates registry changes across cluster nodes over Events Store                   |

## The virtual-subscriber bridge [#the-virtual-subscriber-bridge]

Each registered agent gets its own **virtual subscriber** — an internal broker client that
subscribes to Queries on the agent's channel and forwards them to the agent over HTTP. It
is spawned by the Subscriber Manager when the agent registers and torn down when the agent
deregisters or its TTL expires.

When a Query arrives, the virtual subscriber:

<Steps>
  <Step>
    Extracts the JSON-RPC body from the broker request.
  </Step>

  <Step>
    Unpacks forwarded HTTP headers (the 

    `a2a_hdr_*`

     tags) back into real headers.
  </Step>

  <Step>
    Sets the 

    `X-KubeMQ-Caller-ID`

     header to the original caller's identity.
  </Step>

  <Step>
    Makes an HTTP 

    `POST`

     to the agent's registered URL with 

    `Content-Type: application/json`

    .
  </Step>

  <Step>
    Wraps the agent's HTTP response and publishes it back on the broker reply channel.
  </Step>
</Steps>

The subscriber dispatches on the `a2a_method` tag in the incoming Query: `message/stream`
is routed to the streaming handler (which opens an SSE connection to the agent),
`stream_cancel` is routed to the cancel handler, and everything else — including
`message/send` and `tasks/send` — goes to the synchronous request handler. All broker
operations for an agent use the client ID `a2a-vsub-<agent_id>` (for example
`a2a-vsub-agent-b`).

<Callout type="info">
  Virtual subscribers use a queue subscription with the queue group set to the agent ID, so
  load-balanced delivery works correctly across the cluster.
</Callout>

## Internal channels [#internal-channels]

All agent-platform traffic uses channels under the reserved `_AGENTS_.` prefix. User
channels that start with `_AGENTS_.` are rejected by `IsReservedChannel` — see
[Auth & security](/connectors/reference/auth-and-security).

| Channel                       | Purpose                                                                         | Transport    |
| ----------------------------- | ------------------------------------------------------------------------------- | ------------ |
| `_AGENTS_.agents/<agent_id>`  | Request/reply to the agent via its virtual subscriber (including stream cancel) | Query        |
| `_AGENTS_.stream/<stream_id>` | Temporary SSE stream events relayed by the virtual subscriber                   | Events       |
| `_AGENTS_.discovery`          | Registry replication events across cluster nodes                                | Events Store |

## Header forwarding [#header-forwarding]

Caller HTTP headers ride along to the agent through the broker request's `Tags` field using
an `a2a_hdr_` prefix: the connector packs each allowed header as
`a2a_hdr_<Header-Name>: <value>`, and the virtual subscriber strips the prefix and replays
it as a real HTTP header on the outbound POST.

Hop-by-hop and sensitive headers are **never** forwarded: `Connection`, `Keep-Alive`,
`Transfer-Encoding`, `Te`, `Trailer`, `Upgrade`, `Host`, `Content-Length`,
`Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`, `Proxy-Authenticate`,
`X-Forwarded-For`, and `X-Real-Ip` are dropped.

Regardless of which headers the caller sent, the virtual subscriber always sets
&#x2A;*`X-KubeMQ-Caller-ID`** on the outbound request, carrying the original caller's KubeMQ
client identity — so your agent can always tell who called it. Any KubeMQ transport (gRPC,
REST, the A2A HTTP gateway, or the MCP bridge) can forward headers the same way by setting
`a2a_hdr_*` tags on the request.

## Concurrency control [#concurrency-control]

Each virtual subscriber enforces a **per-agent** concurrency cap using a buffered-channel
semaphore sized to `AgentMaxConcurrency` (default `100`). When a Query arrives and the
semaphore has capacity, a handler goroutine runs and releases its slot on completion. When
all slots are occupied, the overflow Query is **immediately rejected** with a transport
failure — `Executed: false`, `Error: "server busy: concurrency limit reached"` — without
spawning a goroutine.

This keeps a single hot agent from consuming unbounded goroutines under load. The cap, the
response-size limit, and the timeout/gateway-buffer behavior are covered in detail in
[Concurrency & limits](/aiway/a2a/guides/concurrency).

## Timeouts and the gateway buffer [#timeouts-and-the-gateway-buffer]

Each request carries a timeout taken from `params.configuration.timeout` in the JSON-RPC
body, falling back to `DefaultTimeoutSeconds` (`300`) and capped at `MaxTimeoutSeconds`
(`3600&#x60;). Before forwarding, the gateway adds a **`GatewayTimeoutBuffer` of 10 seconds** to
the downstream deadline so the gateway never times out before the agent does. SSE stream
endpoints skip the per-route timeout middleware entirely because they are long-lived; they
are bounded by the idle timeout instead (see [Streaming](/aiway/a2a/streaming)).

## Cluster behavior [#cluster-behavior]

Virtual subscribers are **local to the node where the agent registered** — they are not
replicated. Registry *state*, however, is shared cluster-wide.

<Mermaid
  chart="`
graph LR
AGENT[&#x22;Agent&#x22;]
NODEA[&#x22;Node A<br/>+ virtual subscriber&#x22;]
NODEB[&#x22;Node B<br/>card only&#x22;]
NODEC[&#x22;Node C<br/>card only&#x22;]

AGENT -.->|register| NODEA
NODEA -.->|_AGENTS_.discovery| NODEB
NODEA -.->|_AGENTS_.discovery| NODEC

class AGENT external
class NODEA aiway
class NODEB,NODEC broker
`"
/>

*Only the registering node spawns the virtual subscriber; other nodes store the card and route Queries to it over the cluster mesh.*

* An agent registers on **Node A** → its virtual subscriber is spawned on **Node A only**.
* Replication events over `_AGENTS_.discovery` propagate the `AgentCard` to **Nodes B and C**; they store the card but do **not** spawn a subscriber.
* The cluster mesh routes Queries from any node to the virtual subscriber on Node A.
* If **Node A** goes down, the agent's heartbeat expires on all nodes and its subscribers are cleaned up.
* When the agent re-registers on **Node B**, a fresh virtual subscriber is spawned there.

## Transport vs. application errors [#transport-vs-application-errors]

Because the bridge is the boundary between the broker and HTTP, it cleanly separates two failure
classes via the `Executed` flag on the response:

* **Transport error** (`Executed: false`) — the agent never processed the request: it was unreachable, timed out, returned `502`/`503`/`504`, or its response exceeded the size cap. Safe to retry.
* **Application error** (`Executed: true`) — the agent *did* process the request and returned an HTTP `4xx`/`5xx` with a JSON-RPC error body. Retrying blindly usually will not help.

This distinction lets callers choose the right retry strategy — see
[Error handling](/aiway/a2a/error-handling).

## Related [#related]

<Cards>
  <Card title="Agent registry" href="/aiway/a2a/registry" description="Register, list, heartbeat, and deregister agents; the AgentCard model and TTL liveness." />

  <Card title="Synchronous messaging" href="/aiway/a2a/sync-messaging" description="message/send, context IDs, custom methods, and the Executed error flag." />

  <Card title="Streaming (SSE)" href="/aiway/a2a/streaming" description="How the virtual subscriber relays an agent SSE stream back to the caller." />

  <Card title="Concurrency & limits" href="/aiway/a2a/guides/concurrency" description="Per-agent concurrency cap, response-size limit, and timeout buffers." />
</Cards>
