Observability
Agent-to-Agent (A2A) traffic uses the same telemetry pipelines as model traffic. Usage-event fields identify the caller, agent, method, and outcome, while Prometheus labels let you separate agent traffic from model traffic.
Use these signals to measure agent call volume and monitor failures. Rate limits and upstream errors appear in the same observability tools you already use for model traffic. In AISIX Cloud, budget rejections appear there as well.
Usage Events
AISIX emits an A2A usage event once the gateway can attribute the request to an enabled agent and caller. This includes calls rejected by unsupported upstream authentication, rate limits, upstream failures, or budgets configured through AISIX Cloud. Malformed request bodies can be rejected before a usage event is emitted.
The event goes into the same sink as model usage and identifies the caller, agent, method, outcome, and timing:
| Field | Value |
|---|---|
inbound_protocol | a2a |
a2a_agent_name | The registered agent that was called. |
a2a_method | The JSON-RPC method exactly as the caller wrote it, such as message/send or its 1.0 spelling SendMessage, when AISIX can read it from the request body. |
a2a_operation | The canonical operation a2a_method names. Group by this rather than by the raw method: A2A 0.3 and 1.0 spell one operation two ways, and an unrecognized method becomes unknown. |
a2a_protocol_version | The wire version AISIX announced to the agent, 0.3 or 1.0. |
a2a_task_id | The task the call created or acted on. Use it to gather every request that touched one task across message/send, tasks/get, and tasks/resubscribe. Empty when the call named no task. |
a2a_context_id | The conversation the task belongs to, which ties a multi-turn exchange's tasks together. |
a2a_task_state | The last state the agent reported, normalized to submitted, working, input-required, auth-required, completed, canceled, failed, rejected, or unknown. Empty when no response carried a state. |
a2a_stream_event_count | Events relayed to the caller on a streamed call. 0 on a non-streaming call. |
upstream_ttft_ms | Time to the agent's first streamed event. Read with upstream_latency_ms it separates a stream that said nothing for a long time from one that said plenty. |
prompt_tokens, completion_tokens | Token counts for the message text, counted by the gateway. See Token counts. |
api_key_id | The caller API key that made the call. |
status_code | The call's outcome status. |
upstream_latency_ms | Time spent on the upstream agent call. |
downstream_latency_ms | Total time the caller waited for the agent call. |
request_id, occurred_at | Correlation id and timestamp. |
Token Counts
An A2A agent reports no usage of its own, because the protocol has no usage block. AISIX therefore counts the message text itself, using the gateway's own tokenizer over the text parts of the caller's message and of the agent's reply. Those counts land in prompt_tokens and completion_tokens. The event also carries usage_estimated: true, which says the gateway produced the counts rather than an upstream reporting them. Filter on that flag when you need provider-billed exactness.
Only message/send and message/stream are counted. A read such as tasks/get returns an answer that was already counted when the agent produced it, so counting it again would report one answer once per poll.
cost_usd remains zero. What an agent charges is not something the gateway can observe. These counts are reported only, never charged: they do not add to the caller key's tpm / tpd token windows. Budgets are a separate mechanism — they limit USD spend, and a zero cost adds none. See Traffic Controls.
File and data parts are not counted. Their bytes are not language, and including a base64 blob would distort the estimate.
Agent Gateway currently makes a single upstream attempt that spans the request, so upstream_latency_ms and downstream_latency_ms report the same duration. The separate fields keep A2A records consistent with other gateway traffic, where retries and gateway processing can make the two values differ.
The event does not include request or response message content. Message text reaches an observability exporter only when that exporter is configured for full content capture, and it never reaches the AISIX Cloud control plane.
A2A usage events follow the same delivery paths as model usage events. Any configured observability exporter receives them, so A2A traffic appears alongside the rest of your gateway traffic. In AISIX Cloud, they also flow to the control plane's usage sink.
Metrics
A2A requests appear in the gateway's Prometheus metrics with labels that distinguish them from model traffic. Use the labels below to filter the relevant metric series:
| Goal | Metric | Filter |
|---|---|---|
| Count A2A requests by outcome. | aisix_requests_total | provider="a2a" and model="a2a" |
| Track active A2A requests. | aisix_proxy_in_flight_requests | endpoint="/a2a" and inbound_protocol="a2a" |
| Check A2A usage-event emission. | aisix_usage_events_emitted_total | handler="a2a" |
The aisix_a2a_* family carries the dimension the shared families cannot: which agent was reached and which operation was invoked.
| Metric | Labels | What it answers |
|---|---|---|
aisix_a2a_requests_total | agent, operation, status | Call volume and failure rate for one agent's operation. |
aisix_a2a_ttfb_seconds | agent, operation | How long an agent takes to send its first streamed event. |
aisix_a2a_stream_events_total | agent, operation | Events relayed. Divided by the request count over the streaming operations, it gives events per call. |
aisix_a2a_task_state_total | agent, state | The rate at which calls end on each state, such as a rising share of failed. |
aisix_a2a_requests_total does not agree with aisix_proxy_requests_total{endpoint="/a2a"}. Two differences are deliberate. A call refused before its agent is resolved has no agent to file under, so it is counted in the proxy family only. That covers a bad key, a denied agent, and an unknown one. A stream the caller abandons is a 4xx here but a 2xx there, because the response really did begin as a 200. Read this family for agent health and the proxy family for route traffic.
Task ids, context ids, and JSON-RPC request ids are never metric labels. They are what makes a call traceable, and that is exactly what makes them unusable as label values; they appear in usage events and traces instead.
For usage-event emission, filter by handler. The emission counter keeps its protocol label bounded and groups A2A events under other.
This label behavior applies only to the Prometheus emission counter. The delivered usage event still identifies the traffic as A2A and includes the agent name and method.
Metrics are exposed on GET /metrics through the dedicated metrics listener. For the full metric catalog and label semantics, see Metrics Reference.
Verify Metrics
To verify that A2A metrics are emitted, send one A2A call through the gateway, then scrape the dedicated metrics listener. The example below uses the default listener address and path. If your startup configuration sets a different observability.metrics.prometheus.addr, use that address instead.
Metric families register on first observation, so the A2A series appears only after a call is recorded:
curl -sS "http://127.0.0.1:9090/metrics" | grep -E 'handler="a2a"|provider="a2a"'
The output should include both metric samples:
| Metric | Label |
|---|---|
aisix_usage_events_emitted_total | handler="a2a" |
aisix_requests_total | provider="a2a" |
Next Steps
You now know where A2A calls appear in usage events and metrics. Use these guides to review the full metric catalog or adjust the traffic that produces those signals:
- Metrics Reference: review the full metric catalog and label semantics.
- Rate limits and budgets: apply request limits, concurrency limits, and budgets to A2A calls.
- Control agent access: scope caller API keys to specific agents or every agent.