Belajar A2A - Performance & Scale
Series/Belajar A2A/Episode 18
Episode 18 of 23

Belajar A2A - Performance & Scale

Episode ini membahas cara men-skalakan kolaborasi antar agent: connection pooling, caching agent card, load balancing, tradeoff streaming vs polling, meminimalkan payload, dan kapan memilih gRPC untuk throughput tinggi.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Pendahuluan

Di episode 17 kalian sudah menyusun task pattern tingkat lanjut: fan-out dan fan-in antar agent, alur human-in-the-loop lewat state input-required, hingga idempotency, retry, dan checkpoint untuk task berumur panjang. Semua pola itu berjalan baik ketika jumlah task masih puluhan. Masalah muncul ketika task menjadi ribuan per detik: pola yang benar secara logika bisa berantakan secara teknis.

Episode 18 ini adalah pijakan berikutnya: menjaga latensi dan stabilitas saat skala membesar. Kita akan membahas connection pooling supaya koneksi TCP tidak dibuka-buka setiap task, caching agent card agar traffic discovery tidak membebani, load balancing untuk banyak replika remote agent, tradeoff streaming vs polling, minimalisasi payload, dan kapan harus beralih ke binding gRPC untuk throughput tinggi.

Connection Pooling: Jangan Membuka Koneksi Baru Setiap Task

Setiap handshake TCP dan TLS memakan waktu puluhan milidetik — murah untuk sekali, mahal untuk seribu kali. Client A2A yang ditulis naif biasanya membuka koneksi baru per task. Solusinya adalah connection pooling: satu koneksi HTTP keep-alive yang dipakai ulang untuk banyak request.

Pythonpool-httpx.py
import httpx
from a2a_sdk import A2AClient
 
transport = httpx.AsyncHTTPTransport(
    limits=httpx.Limits(max_connections=50, max_keepalive_connections=20)
)
 
async with httpx.AsyncClient(transport=transport, timeout=30.0) as client:
    a2a = A2AClient(client)
    for task in daftar_task:
        result = await a2a.send_task(task)

Perhatikan dua parameter httpx.Limits: max_connections membatasi total koneksi yang boleh terbuka, sedangkan max_keepalive_connections menentukan berapa koneksi idle yang disimpan hidup untuk pemakaian berikutnya. Nilai ini harus disesuaikan dengan batas koneksi di sisi remote agent — kalau client membuka 50 koneksi tapi server hanya menampung 20, sisanya mengantre di kernel. Aturan praktis: mulai dari 10-20 keepalive per host, lalu ukur.

Info

Connection pooling bekerja paling baik untuk remote agent yang dipanggil sering dalam satu proses. Untuk workload sekali-panggil (misalnya serverless yang memulai proses baru per request), pooling justru menambah kompleksitas tanpa manfaat — biarkan koneksi mati secara alami.

Caching Agent Card: Kurangi Traffic Discovery

Sebelum task dikirim, client harus mengambil agent card untuk tahu kemampuan remote agent. Jika di-fetch setiap task, ini memboroskan bandwidth dan menambah latensi tiap request. Agent card jarang berubah — beda seminggu pun masih valid. Solusinya: cache dengan TTL.

Pythoncache-agent-card.py
import time
 
class AgentCardCache:
    def __init__(self, ttl_seconds=3600):
        self.ttl_seconds = ttl_seconds
        self._store = {}
 
    def get(self, url: str):
        entry = self._store.get(url)
        if entry and time.time() - entry["ts"] < self.ttl_seconds:
            return entry["card"]
        return None
 
    def set(self, url: str, card: dict):
        self._store[url] = {"card": card, "ts": time.time()}

Setelah card di-cache, client bisa langsung mengirim tasks/send tanpa menunggu round-trip discovery. TTL harus lebih pendek dari jadwal rilis versi agent kalian: jika agent di-deploy beberapa kali sehari, turunkan TTL ke 300 detik; jika jarang berubah, 3600 detik aman. Untuk signed agent card (sudah dibahas di episode 9 dan akan diperdalam di episode 21), cache juga harus menyimpan hasil verifikasi tanda tangan agar verifikasi kriptografis tidak diulang.

Load Balancing Banyak Remote Agent

Ketika satu remote agent sudah tidak sanggup menampung beban, jawabannya adalah replika — dan replika butuh load balancer. Tantangan khusus A2A: task itu stateful. Task yang sudah jalan di replika A harus dilanjutkan di replika A; jika diarahkan ke replika B, client tidak akan menemukan state-nya.

load-balancer.yaml
upstream a2a-agent {
    least_conn;
    server agent-a.internal:3000;
    server agent-b.internal:3000;
    server agent-c.internal:3000;
}
 
server {
    listen 443 ssl;
    location / {
        proxy_pass http://a2a-agent;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Cookie $http_cookie;
    }
}

Strategi least_conn cocok karena task A2A punya durasi bervariasi — replika yang menerima task berat akan lebih sepi di koneksi berikutnya. Tapi kunci sebenarnya ada di sticky session: jika task state disimpan per-replika, pastikan client yang sama selalu mendarat di replika yang sama. Alternatif yang lebih kokoh adalah menyimpan state task di store bersama (Redis, database) sehingga replika mana pun bisa melanjutkan — lalu load balancer bisa memakai strategi murni tanpa stickiness. Jangan lupa health check memakai tasks/get dengan task dummy, bukan hanya ping TCP.

Streaming vs Polling: Tradeoff

Episode 7 membahas SSE streaming dan push notification. Di level performa, keputusan ini menjadi tradeoff nyata:

  • Streaming (SSE): latensi per-event terendah — client melihat delta hasil begitu tersedia. Tapi koneksi panjang dipertahankan, sehingga memakai slot connection pool lebih lama dan rawan putus di proxy idle timeout.
  • Polling: satu koneksi per request, singkat dan selesai. Simpel, lolos proxy apa pun, dan alami untuk retry. Harganya: latensi rata-rata naik sebesar separuh interval polling.
Pythonpolling-backoff.py
async def poll_task(client, task_id: str, timeout: int = 120):
    deadline = time.time() + timeout
    delay = 1.0
    while time.time() < deadline:
        task = await client.get_task(task_id)
        if task.status not in {"submitted", "working"}:
            return task
        await asyncio.sleep(delay)
        delay = min(delay * 1.5, 10.0)
    raise TimeoutError("task terlalu lama")

Polling dengan exponential backoff adalah kompromi terbaik untuk mayoritas kasus: task pendek terdeteksi cepat, task panjang tidak menebak interval tetap. Gunakan streaming hanya ketika interaktivitas real-time itu bernilai — misalnya agent menampilkan hasil parsial kepada user yang menunggu.

Payload Minimization

Payload besar adalah musuh latensi diam-diam. Antrean jaringan diisi data yang tidak perlu, dan parsing JSON memakan CPU di kedua sisi. Tiga teknik yang langsung terasa:

  • File part dengan URL, bukan dataURI inline — part file punya dua bentuk: dataURI (isi byte disematkan) atau URL (referensi). Untuk file besar, kirim URL; isi file di-download terpisah, paralel, dan bisa di-cache.
  • Structured part yang ringkas — gunakan schema JSON terkecil yang cukup, bukan menyalin seluruh record database. Kirim id, nama, dan harga saja, bukan 40 kolom.
  • Hindari duplikasi konteks — jika pesan berulang, kirim sekali lalu rujuk dengan id, jangan ulang isinya di setiap message.
payload-ringkas.json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "message/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [
        { "type": "text", "text": "Ambil detail order ini" },
        {
          "type": "file",
          "url": "https://cdn.internal/orders/8321.json",
          "mimeType": "application/json"
        }
      ]
    }
  }
}

Perhatikan bagian file di atas: file 2 MB tidak dikirim lewat wire A2A — hanya URL-nya. Ini yang membedakan sistem yang sanggup ratusan task per detik dari yang macet di 20 task per detik.

gRPC untuk Throughput Tinggi

Kalau pooling, payload kecil, dan polling masih belum cukup — misalnya kawanan agent mengirim task sepanjang hari dengan volume sangat tinggi — saatnya binding gRPC. Keunggulan utamanya:

  • Multiplexing: banyak stream dalam satu koneksi HTTP/2, tanpa head-of-line blocking.
  • Protobuf biner: payload jauh lebih kecil daripada JSON dan parsing-nya jauh lebih murah.
  • Backpressure native: aliran data berhenti otomatis ketika penerima lambat, sehingga memori tidak jebol.
agent.proto
syntax = "proto3";
 
message Task {
  string id = 1;
  string status = 2;
  repeated string messages = 3;
}
 
message TaskQuery {
  string id = 1;
}
 
service A2AAgentService {
  rpc SendMessage(stream TaskQuery) returns (stream Task);
  rpc GetTask(TaskQuery) returns (Task);
}

gRPC bukan pengganti — ia memakai ulang server handler yang sama dengan binding HTTP/JSON (kita sudah bahas gateway-nya di episode 11). Strategi yang sehat: jadikan HTTP/JSON sebagai default publik, dan gRPC sebagai jalur internal antar agent yang saling kenal. Client internal yang sibuk bisa langsung bicara gRPC, sementara ekosistem luar tetap memakai JSON-RPC standar.

Penutup

Episode 18 menaikkan level dari "task berjalan benar" menjadi "task berjalan benar dalam ribuan". Connection pooling memotong biaya handshake, caching agent card meniadakan traffic discovery berulang, load balancing menyebar beban sambil menjaga state task, polling backoff menyeimbangkan latensi dan kesederhanaan, payload ringkas mengosongkan bandwidth, dan gRPC membuka jalur throughput tinggi antar agent internal.

Inti yang harus dibawa pulang:

  • Pooling koneksi dan caching agent card adalah dua kemenangan performa pertama yang paling murah.
  • Task A2A itu stateful — desain load balancing dan penyimpanan state task secara bersamaan.
  • Streaming hanya untuk kasus yang butuh latensi per-event; polling backoff cukup untuk sisanya.
  • Minimalkan wire: URL untuk file, structured part ringkas, dan tanpa duplikasi konteks.
  • gRPC untuk jalur internal yang sibuk, HTTP/JSON tetap menjadi bahasa publik.

Di episode 19 kita masuk wilayah yang lebih menarik: ekosistem x402 dan commerce — bagaimana agent bisa membayar agent lain lewat ekstensi HTTP 402, mandate, dan approval. Sampai jumpa!

Belajar A2A - Performance & Scale | Belajar A2A