How It Works
Inside the A2A gateway — the virtual-subscriber bridge, internal channels, header forwarding, concurrency, and cluster behavior.
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
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.
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.
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
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:
a2a_hdr_* tags) back into real headers.X-KubeMQ-Caller-ID header to the original caller's identity.POST to the agent's registered URL with Content-Type: application/json.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).
Virtual subscribers use a queue subscription with the queue group set to the agent ID, so load-balanced delivery works correctly across the cluster.
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.
| 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
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
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
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.
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). 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).
Cluster behavior
Virtual subscribers are local to the node where the agent registered — they are not replicated. Registry state, however, is shared cluster-wide.
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_.discoverypropagate theAgentCardto 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
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, returned502/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 HTTP4xx/5xxwith a JSON-RPC error body. Retrying blindly usually will not help.
This distinction lets callers choose the right retry strategy — see Error handling.
Related
Agent registry
Register, list, heartbeat, and deregister agents; the AgentCard model and TTL liveness.
Synchronous messaging
message/send, context IDs, custom methods, and the Executed error flag.
Streaming (SSE)
How the virtual subscriber relays an agent SSE stream back to the caller.
Concurrency & limits
Per-agent concurrency cap, response-size limit, and timeout buffers.
Was this page helpful?
Configuration
Configure the A2A connector — enable model, timeouts, agent TTL, concurrency, and response limits via YAML, environment variables, or Docker.
Agent Registry
Register, list, heartbeat, and deregister AI agents through the A2A REST API — agent cards, ownership, TTL liveness, and the MaxAgents limit.