# Polling Transport

> Use polling instead of SSE for environments that can't maintain long-lived HTTP connections.

**URL:** https://inference.sh/docs/api/sdk/polling
**Last updated:** 2026-10-11

---

Use polling instead of SSE for environments that can't maintain long-lived HTTP connections.

---

## When to Use Polling

Some environments cannot hold open Server-Sent Events (SSE) connections:

- **Convex actions** — HTTP requests must complete within a timeout
- **Cloudflare Workers** — no long-lived inbound connections
- **Restricted edge runtimes** — firewalls or proxies that drop idle connections
- **Serverless functions** — Lambda, Cloud Functions with short timeouts

In these cases, set `stream: false` to switch from SSE to lightweight polling. The SDK automatically polls a status endpoint and only fetches full data when the status changes.

---

## Client-Level Default

Set polling as the default transport for all operations:

```typescript
import { inference } from '@inferencesh/sdk';

const client = inference({
  apiKey: 'inf_your_key',
  stream: false,          // Use polling instead of SSE
  pollIntervalMs: 2000,   // Poll every 2 seconds (default)
});
```

All `client.run()`, `agent.sendMessage()`, and `AgentChatProvider` calls will use polling unless overridden.

---

## Task Polling

Override the transport per-call with `stream: false` on `run()`:

```typescript
const result = await client.run(
  { app: 'infsh/flux', input: { prompt: 'A sunset' } },
  {
    stream: false,
    pollIntervalMs: 3000,  // Optional: override interval
    onUpdate: (task) => {
      console.log('Status:', task.status);
    },
  }
);

console.log('Output:', result.output);
```

**How it works:**
1. Task is created via `POST /apps/run` (same as streaming)
2. SDK polls `GET /tasks/{id}/status` every `pollIntervalMs`
3. When the status changes, it fetches the full task via `GET /tasks/{id}`
4. `onUpdate` fires with the full task data
5. Promise resolves on completion, rejects on failure or cancellation (the error message comes from the task's `error` field when present)

When status polling cannot recover (for example repeated network errors), set `maxReconnects` on `run()` options (default `5`) to control retry behavior.

---

## Agent Polling

### Imperative SDK

```typescript
const client = inference({ apiKey: 'inf_your_key' });

const agent = client.agent('my-team/support-agent@latest');

const response = await agent.sendMessage('Hello!', {
  stream: false,
  pollIntervalMs: 2000,
  onMessage: (message) => {
    console.log('Message:', message.content);
  },
  onChat: (chat) => {
    console.log('Chat status:', chat.status);
  },
  onToolCall: (invocation) => {
    console.log('Tool call:', invocation.name);
  },
});
```

**How it works:**
1. Message is sent via `POST /agents/run` (same as streaming)
2. SDK polls `GET /chats/{id}/status` every `pollIntervalMs`
3. When the status changes, it fetches chat metadata via `GET /chats/{id}` and dispatches `onChat`
4. New and updated messages arrive from `chat_messages` stream events (streaming mode) or require a separate `GET /chats/{id}/messages` call — `GET /chats/{id}` does not include message history
5. Client tool invocations (`in_progress`, or `awaiting_input` for backwards compatibility) are dispatched to `onToolCall`
6. Promise resolves when the chat becomes `idle`

Transient status poll failures (for example a single network error on `GET /chats/{id}/status`) log a `[Agent] Poll error:` warning and polling continues — `sendMessage` does not reject until the chat reaches a terminal state or the initial send fails. Use `onError` on `AgentChatProvider` to surface persistent transport failures to your UI.

### React Provider

```tsx
import { AgentChatProvider, useAgentChat } from '@inferencesh/sdk/agent';

function App() {
  return (
    <AgentChatProvider
      client={client}
      agentConfig={{ agent: 'my-team/support-agent@latest' }}
      stream={false}
      pollIntervalMs={2000}
    >
      <ChatUI />
    </AgentChatProvider>
  );
}
```

**How it works (React provider):**
1. First message: `POST /chats` (create chat with agent ref), then `POST /chats/{id}/messages`
2. Follow-ups: `POST /chats/{id}/messages` only
3. SDK polls `GET /chats/{id}/status` every `pollIntervalMs`
4. When the status changes, the SDK fetches chat metadata (`GET /chats/{id}`) and message history (`GET /chats/{id}/messages`) and merges them into chat state — `GET /chats/{id}` alone does not include messages
5. New and updated messages are available through `useAgentChat()` (`chat.chat_messages`)
6. `sendMessage` resolves when the POST completes; assistant replies arrive via polling or SSE

The provider uses the same status-polling mechanism as the imperative SDK after the message is sent. All hooks (`useAgentChat`, `useAgentActions`) work identically regardless of transport.

### Loading older messages

Long chats load only the **50 newest messages** on connect. **v0.6.41+:** use `useAgentActions().loadOlderMessages()` to fetch older pages — the provider tracks the cursor from the initial load, prepends deduplicated messages to `useAgentChat().messages`, and returns `true` when more pages exist. Check `hasOlderMessages` before showing a scroll-up affordance. Works the same with SSE and polling.

```tsx
const { messages } = useAgentChat();
const { loadOlderMessages, hasOlderMessages } = useAgentActions();

// Call when the user scrolls near the top of the message list
if (hasOlderMessages) {
  await loadOlderMessages();
}
```

Optional lifecycle callbacks (available on `AgentChatProvider` with both SSE and polling):

| Prop | When it fires |
|------|---------------|
| `onChatCreated` | A new chat ID is assigned after the first message |
| `onStatusChange` | Connection status changes (`connecting`, `streaming`, `idle`). **v0.23.0+** reports `connecting` while the chat stream waits to retry after a network drop. It is never called with `error`: when reconnecting gives up, `onError` fires and `useAgentChat().connectionStatus` becomes `'error'` |
| `onTurnEnd` | The agent's turn ends — chat status transitions from `busy` to `idle`, `completed`, or another non-busy state. Receives the full `ChatDTO`. Use to refetch host data after agent tool actions without polling or window-focus workarounds. |
| `onError` | Stream or poll transport errors, failed sends, or tool submission failures |

```tsx
<AgentChatProvider
  client={client}
  agentConfig={{ agent: 'my-team/support-agent@latest' }}
  stream={false}
  onError={(error) => console.error('Agent error:', error)}
  onStatusChange={(status) => console.log('Status:', status)}
  onTurnEnd={(chat) => refetchDashboard(chat.id)}
>
  <ChatUI />
</AgentChatProvider>
```

`onError` on the provider is separate from the client-level `onError` in `inference({ onError })`, which handles REST, `HttpClient.fetch()`, and failed initial stream connections (SSE and NDJSON). See [Error Handling](/docs/api/sdk-javascript#error-handling).

---

## Configuration Reference

| Option | Type | Default | Scope |
|--------|------|---------|-------|
| `stream` | `boolean` | `true` | Client, `run()`, `sendMessage()`, Provider |
| `pollIntervalMs` | `number` | `2000` | Client, `run()`, `sendMessage()`, Provider |
| `maxReconnects` | `number` | `5` | `run()` when `stream: false` |

**Precedence:** Per-call options override provider props, which override client defaults.

---

## Comparison: Streaming vs Polling

| | SSE (default) | Polling |
|---|---|---|
| **Latency** | Real-time | Up to `pollIntervalMs` delay |
| **Partial updates** | Yes (`onPartialUpdate`) | No (full fetch only) |
| **Connection** | Long-lived HTTP | Short-lived requests |
| **Edge compatible** | No | Yes |
| **Bandwidth** | Lower (deltas only) | Higher (status checks + full fetches) |

Use SSE when you can. Use polling when you must.
