When to use a2a protocol instead of cramming every agent into one shared framework is a question that appears the moment you scale past a single Python process. The decision is not about which LLM you call; it is about ownership, independent deployability, and what happens when one agent crashes. This guide lays out an ordered path to make that call without overthinking it.
The core distinction
A shared agent framework means your agents are objects or modules inside one runtime, sharing memory, types, and a single dependency tree. A2A (agent-to-agent) means each agent exposes a network endpoint and speaks a message contract. You pay serialization and latency costs, but you gain isolation.
If you cannot articulate why two pieces of logic need separate failure domains, you probably don’t need A2A yet.
Step 1: Map agent boundaries and team ownership
List every autonomous unit you plan to build. For each, note:
- Which team owns it
- What data it is allowed to touch
- Whether it has a separate release cadence
If two agents are owned by the same team, share the same DB credentials, and deploy together, a shared framework is simpler. The moment one agent is owned by a different team or has stricter compliance scope, you have a boundary worth enforcing with a protocol.
When to use a2a protocol becomes clear when organizational boundaries mirror runtime boundaries.
Step 2: Assess deployment independence
Ask: can agent A ship a breaking change without re-testing agent B? If no, they are coupled; keep them in a monorepo with a shared framework. If yes, define an HTTP or message-queue contract.
A minimal A2A envelope does not need a standard body yet. Start with:
{
"sender": "agent_pricing",
"receiver": "agent_checkout",
"task": "quote_cart",
"payload": {"items": [{"sku": "X1", "qty": 2}]},
"reply_to": "https://pricing.internal/a2a/inbox"
}
The receiver validates the task string and returns a structured response. No shared Python class required.
Step 3: Evaluate model access and runtime dependencies
Agents often differ in which models they call. One might use a cheap classifier; another needs a frontier reasoning model. If you standardize on a single inference gateway, that concern is centralized. Even if you route through one OpenAI-compatible endpoint that addresses 240+ models with automatic fallback, like n4n.ai, the agent coordination problem remains separate.
The point: model routing is not a reason to adopt A2A by itself. But if agent A runs on edge containers and agent B runs in a GPU cluster, a shared framework is physically impossible. That is a hard trigger for when to use a2a protocol.
Step 4: Define the message contract explicitly
A shared framework gives you compile-time type checks. A2A gives you a schema file. Write it down in JSON Schema or Protobuf. Example receiver in FastAPI:
from fastapi import FastAPI
from pydantic import BaseModel
class A2AMsg(BaseModel):
sender: str
receiver: str
task: str
payload: dict
reply_to: str | None = None
app = FastAPI()
@app.post("/a2a/inbox")
async def inbox(msg: A2AMsg):
if msg.task == "quote_cart":
total = sum(i["qty"] * 10 for i in msg.payload["items"])
return {"status": "ok", "result": {"total": total}}
return {"status": "unknown_task"}
The sender can be a different language entirely. That is the win.
Step 5: Prototype with real failure modes
Most A2A designs die in retry handling. Build a sender that:
- Posts the message
- Expects a correlation id
- Times out after 5s
- Retries with idempotency key
import uuid, requests
def send_a2a(url, msg):
msg["correlation_id"] = str(uuid.uuid4())
msg["idempotency_key"] = msg["correlation_id"]
for attempt in range(3):
try:
r = requests.post(url, json=msg, timeout=5)
if r.status_code == 200:
return r.json()
except requests.Timeout:
continue
return {"status": "failed"}
If you skip this, you will get duplicate side effects. A shared framework call is just a function return; A2A is distributed systems.
Step 6: Instrument and trace
With a shared framework you get stack traces. With A2A you get network hops. Emit a trace id in every message and log both sides. Without this, debugging is guessing.
When to use a2a protocol should also consider your observability maturity. If you have no distributed tracing, the operational tax may outweigh the benefit for low-stakes agents.
Common pitfalls and tradeoffs
Schema drift. One team adds a field; the other doesn’t read it. Use a versioned path: /v1/a2a/inbox.
Latency. A function call is microseconds; a network call is milliseconds. If agent A calls agent B 50 times per request, keep them in-process.
Partial failure. Agent B processed the task but its response dropped. Design tasks to be idempotent or use a status endpoint.
Over-partitioning. Junior teams split agents by noun (“user agent”, “order agent”) with no real boundary. You get a distributed monolith.
Decision checklist
Use A2A when:
- Agents are owned by different teams or deploy on different schedules
- Runtime environments are heterogeneous (browser, server, edge)
- Failure of one agent must not crash others
- You need polyglot implementation (Rust sender, Python receiver)
Stick with a shared framework when:
- All agents live in one repo and one process
- Latency between agents is on the critical path
- You lack tracing infrastructure
- The boundary is hypothetical, not enforced by compliance or scale
When to use a2a protocol is ultimately a question of coupling vs isolation. Pick the side that matches your organizational reality, not the hype cycle. Build the smallest possible message contract, instrument it, and add agents only when the boundary proves real.
A minimal path to start
- Write down two agents and the one task that crosses them.
- Define a JSON envelope with sender, receiver, task, payload, reply_to.
- Implement the receiver as a plain HTTP endpoint.
- Add a correlation id and a 5s timeout in the sender.
- Run it across two processes on your laptop.
- Break the receiver deliberately; confirm the sender survives.
If that exercise feels natural, you have your answer. If it feels like bureaucracy for a single binary, delete the endpoints and import the module.
The protocol is a tool, not a mandate. Use it where the seam is real.