# How does an AI agent wait for a reply without polling?

*2026-10-21 · By Naol Ketema, founder of Agent Identity*

**Short answer:** An AI agent waits for a reply without polling by blocking on a durable event log: it makes one long-poll request that the server holds open until a matching event arrives or a timeout passes, then re-arms. Because the log is durable and ordered, the agent passes a `since` cursor so nothing that happened while it was busy or offline is missed. In Agent Identity this is `client.events.wait()` (or `GET /v1/events/wait`), and `client.events.subscribe()` runs it as a loop for you.

## What does waiting for a reply mean for an agent?

Most useful agent work has a gap in the middle. The agent sends an email and needs the customer's answer. It hands a task to another agent and needs the result. It cannot finish until something outside its control happens.

There are three ways to bridge that gap: poll on a timer, get called by a webhook, or block on an event stream until the thing you care about shows up. For an agent running as a loop, the third is usually the right one.

## Why polling is the wrong default

Polling means calling a list endpoint every N seconds and diffing the result. It works, but it has three costs that compound as you add agents:

- Wasted requests. Almost every poll returns nothing new, and each one counts against rate limits.
- Added latency. On average you see a reply half a poll interval after it arrived. Shorten the interval and you pay more requests.
- Bookkeeping. You have to remember what you already handled, compare lists, and cope with items that changed between two polls.

None of that is the agent's actual job. An LLM loop that spends its time diffing inbox listings is also harder to read and test.

## What a durable event log and a long-poll wait give you

A durable event log is an append-only, ordered record of everything that happened: mail received, mail bounced, a task created, a task answered. A long-poll wait is a single HTTP request that the server holds open until an event matching your filter is appended, or until a timeout passes.

- One request per event instead of one request per poll interval.
- The reply is handed to the agent as soon as it is appended, not on the next tick.
- A timeout is a normal result, not an error. The agent simply waits again.
- Because the log is durable, an agent that was offline or busy can pass a cursor and pick up exactly where it left off.

In Agent Identity, `GET /v1/events/wait` returns at most one event per request and always answers `200`: either `{ "event": {...}, "timed_out": false }` or `{ "event": null, "timed_out": true }`. The wait is capped at 55 seconds (`timeout_ms` from 0 to 55000, default 25000) so it lands inside common proxy idle timeouts. Longer waits are built by re-arming, not by raising the value.

## The race you have to avoid

A wait matches only events created after it starts. If you send an email, then start waiting, and the reply arrives in between, a wait without a cursor will never see it. Fast autoresponders and agents on the same platform make this more likely than it sounds.

- Cursor pattern: capture `const since = new Date()` before the call that triggers the reply, and pass it as `since`. For the next wait, advance `since` to the returned event's `createdAt`.
- Concurrent pattern: start the wait promise first, then make the triggering call, then await the wait.
- Never write a bare `await send(); await wait();` with no `since`. That is the race.

## How to do it with Agent Identity

Send from the agent's mailbox, then wait for `mail.received` on the same thread. The SDK is `@agentidentity/sdk`; the API key here is the agent's own key.

```ts
import { AgentClient } from "@agentidentity/sdk";

const client = new AgentClient({
  baseUrl: "https://api.agent-identity.dev",
  apiKey: process.env.AID_KEY!,
});
const identityId = process.env.AID_AGENT!;

const since = new Date();
const sent = await client.mail.send({
  identityId,
  to: "customer@example.com",
  subject: "Order 1142",
  text: "Can you confirm the delivery address?",
  idempotencyKey: "order-1142-address-check",
});

const { event, timedOut } = await client.events.wait({
  type: "mail.received",
  identityId,
  filter: { threadId: sent.threadId },
  since,
  timeoutMs: 55_000,
});

if (!timedOut && event) {
  console.log(event.payload.from, event.payload.subject);
}
```

For an agent that runs forever, use `client.events.subscribe()`. It advances its cursor from each event's own `createdAt` rather than your clock, re-polls when the server window elapses, and retries failures with exponential backoff.

```ts
for await (const event of client.events.subscribe({ type: "mail.received", identityId })) {
  const thread = await client.mail.getThread(identityId, event.payload.threadId);
  const reply = await compose(thread); // your LLM call

  await client.mail.send({
    identityId,
    to: [event.payload.from!],
    subject: "Re: " + (thread.subject ?? ""),
    text: reply,
    inReplyToMessageId: event.payload.messageId,
    idempotencyKey: "reply-" + event.payload.messageId,
  });
}
```

The same wait works for agent-to-agent work: after `client.a2a.sendTask()`, wait on `a2a.sent_task.updated` with `aggregateId` set to the task id. From a terminal, `aid events tail --type mail.received --agent <id>` re-arms the wait after each event so you can watch the log live. We run our own sales agent this way: it subscribes to the log and only wakes when something arrives.

## When you might not need this

If your agent runs as a serverless function with no long-lived process, a webhook fits better: the platform calls you. If you only check for replies once a day in a batch job, a plain list call is fine. Long-poll waits shine when the agent is a process that would otherwise sit in a sleep loop.

## FAQ

### What happens if no reply arrives within 55 seconds?

The wait returns `timedOut: true` with no event, as a normal `200` response. Call wait again with the same `since`; nothing is lost because the log is durable.

### Can one wait watch several event types?

No. Filters are exact matches and there is no wildcard or multi-type wait. Run one wait per type, or register a webhook for several types.

### What if my agent crashes between receiving an event and replying?

Restart from the last `createdAt` you processed, and derive the reply's `idempotencyKey` from the inbound message id. A retried send with the same key returns the original result instead of sending twice.

### Is this the same as wait_for_event?

Yes. `wait_for_event` is the concept; `GET /v1/events/wait` is the endpoint and `client.events.wait()` is the SDK method.

---
Docs: https://www.agent-identity.dev/docs · All guides: https://www.agent-identity.dev/guides · llms.txt: https://www.agent-identity.dev/llms.txt