Why architecture matters here

Polling is the default alternative, and at small scale it works: the client calls tasks/get every few seconds until the task reaches a terminal state. At real scale it fails in both directions at once. Poll too often and a fleet of client agents turns the remote server into a read-amplification victim — thousands of requests per completed task, almost all of them returning still working. Poll too rarely and the results of an urgent task sit unread for minutes, which in a multi-agent chain compounds: five agents in a pipeline, each adding a lazy poll interval, turn a two-minute job into a twenty-minute one.

Streaming closes the latency gap but keeps a connection pinned. SSE works beautifully for a task measured in seconds; for one measured in hours it demands that the client process stay alive, keep the socket open across NATs and load balancer idle timeouts, and resubscribe correctly after every blip. For serverless clients — a Lambda that fired the task and exited — there is simply no process left to hold the stream.

Push inverts the responsibility: the party that knows when something changed initiates the exchange. But it also inverts the trust relationships. In polling, the client authenticates to the server and reads its own task; nothing new to secure. In push, the server calls out to an arbitrary URL (server-side request forgery risk, data exfiltration risk) and the client accepts unsolicited inbound traffic (spoofing risk, replay risk). The architecture below exists precisely to make this inversion safe, because the naive version — POST the task result as JSON to whatever URL was registered — is an incident report waiting for a timestamp.

There is also an ecosystem argument. A2A exists so agents built by different teams and vendors can interoperate; push notifications are where that interoperability is most fragile, because both sides must agree not just on a schema but on delivery semantics — what a notice means, how it is authenticated, what the receiver may assume. A server that treats push as best-effort talking to a client that treats it as a reliable stream will work in every demo and lose exactly one critical update in production. Getting the contract explicit — at-least-once, unordered, content-free hints, signed and replay-bounded — is what lets two organizations that have never met operate a dependable async channel between their agents.

Advertisement

The architecture: every piece explained

Registration and the config object. The client attaches a pushNotificationConfig when sending the message or afterwards via tasks/pushNotificationConfig/set. It carries the webhook URL, an opaque client token the server will echo back, and an authentication block describing how the server should prove itself. The server validates the URL at registration time, not delivery time: scheme must be HTTPS, the host must not resolve into private or link-local ranges, and mature deployments require an ownership proof — a one-time GET with a challenge nonce that the webhook must echo — before the config is accepted.

The notifier pipeline. Task state transitions — working to input-required, completed, failed — are written to the task store first, then a notification record is enqueued. Decoupling matters: webhook delivery has unbounded latency and must never sit inside the task-execution path. Delivery workers pull from the queue, sign the notice, POST it, and apply exponential backoff with jitter on failure; after the retry budget is exhausted the record lands in a dead-letter queue with the response history attached.

Signing. The notice body stays deliberately small — task id, new state, timestamp — and the server signs a JWT over it: issuer set to the server identity, audience set to the webhook, short expiry, and a key published at a JWKS endpoint so clients can rotate trust without redeploying. The client token from the config rides along so the receiver can route the notice to the right internal tenant cheaply before any cryptography runs.

The receiver. The client webhook does four things and nothing else: verify the signature against the server JWKS, check the timestamp against a replay window, dedupe on (task id, state, timestamp), and acknowledge with a 200 in milliseconds. Actual work — calling tasks/get to fetch the authoritative task with its artifacts, resuming a paused workflow, notifying a human — happens asynchronously behind an internal queue. The notification is a doorbell, not a delivery; the package is always fetched over the authenticated A2A channel the client already trusts.

Config lifecycle and tenancy. Push configs are mutable, per-task state with their own API surface: a client can replace the webhook mid-task when its own infrastructure moves, register more than one config so a monitoring system and the originating agent are both notified, or delete the config to fall back to pure polling. Each config carries its own token and audience, so revocation is per-consumer rather than per-task. On multi-tenant servers the config store is also a quota surface — bound configs per task and per tenant, and rate-limit registration, because an attacker who can register thousands of webhooks has turned your notifier into a distributed HTTP cannon. Config changes are audited events: when a task's results started flowing to a new endpoint is exactly the question an incident review will ask.

A2A push notifications — webhooks for tasks that outlive the connectionnotify small, fetch authoritative stateClient agentregisters pushNotificationConfigRemote A2A serverruns the long taskTask storestates + artifactsURL validatorSSRF + ownership checkNotifier queuestate changes enqueuedSignerJWT: iss, aud, taskId, iatDelivery workersretries + backoff + DLQDead letterundeliverable noticesClient webhook receiververify sig, dedupe, ack 200 fasttasks/get re-fetchauthoritative task stateClient task trackerreconcile + resume workOps — delivery success rate, DLQ depth, webhook latency, key rotation, replay window alarmstask updateenqueueread stateat registerPOST + JWTsigndeliveron notifyupdate
A2A push notification path: task state changes flow through a queue, get signed as compact JWTs, and are delivered with retries to a client webhook that verifies, dedupes, and re-fetches the authoritative task before acting.
Advertisement

End-to-end flow

A client agent sends message/send with a research request and a pushNotificationConfig pointing at https://agents.example.com/hooks/a2a. The server validates the URL — public host, HTTPS, DNS resolves outside RFC 1918 space — fires the challenge GET, receives the echoed nonce, and persists the config next to the freshly created task. The response returns the task in state submitted, and the client goes away entirely; nothing about its runtime needs to survive.

Forty minutes later the remote agent finishes its work and the task transitions to completed. The transition commits to the task store; an outbox row is enqueued in the same transaction so a crash between the two cannot lose the notice. A delivery worker picks it up, mints a JWT — iss the server, aud the webhook URL, taskId, state: completed, iat now, expiry five minutes out — and POSTs the compact notice. The first attempt hits a deploy-window 503; the worker backs off 30 seconds and the second attempt returns 200 in 80 ms.

On the client side, the webhook verifies the signature against the cached JWKS, sees the timestamp inside the replay window, checks the dedupe store — unseen — and acks. An internal job wakes the task tracker, which calls tasks/get with its own credentials, receives the full task with three artifacts, marks the workflow step complete, and hands the artifacts to the next agent in the chain. Had the notification never arrived — webhook down for an hour, notice dead-lettered — the tracker's slow reconciliation loop, polling unfinished tasks every ten minutes, would have caught the completion anyway. Push is the fast path; reconciliation is the guarantee.

The same machinery carries the mid-task states, and they matter more than the terminal ones. Twenty minutes in, the remote agent needs a clarification and moves the task to input-required; the notice reaches the webhook in seconds, the tracker re-fetches, sees the agent's question in the task status message, and routes it to the human who owns the workflow. Without push, that question would have sat unanswered until the next poll — and a task blocked on input for fifty minutes looks, to every dashboard, identical to a task making progress. The latency win of push notifications is not really about completions; it is about unblocking the states where a human or another agent must act before anything else can happen.