cloudflare / cloudflare/agents
Agent WebSockets have no heartbeat: an idle socket is dropped without a close frame and the client keeps sending into it
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
**TL;DR:** Nothing in the stack sends traffic on an idle Agent WebSocket, so the network drops it after a minute or two of silence, with no close frame. The browser still reports `OPEN`, partysocket never reconnects, and the next `useAgentChat` message is lost silently. Cloudflare's own docs prescribe the fix (a client-side ping answered by `setWebSocketAutoResponse`), and the SDK is the right place for it.
## Versions
- `agents@0.22.0`, `@cloudflare/think@0.17.0`, `partysocket@1.3.0`
- Same on `main` as of 2026-09-10 (`b9142be`): no ping in `AgentClient`/`useAgent`, no `setWebSocketAutoResponse` anywhere in the repo, `acceptWebSocket(ws, tags)` called bare in `packages/agents/src/lifecycle/durable-object-lifecycle.ts`
## Reproduce
1. Open a `useAgentChat` pane against a deployed Agent (ours: a Worker forwarding the upgrade to the Agent's Durable Object with `stub.fetch(request)`, the documented pattern).
2. Do nothing for two minutes.
3. Send a message.
Result: the message never arrives and nothing reports it. Wrapping `window.WebSocket` to log open/close/frames shows why:
| idle before send | result |
|---|---|
| ~0 s | answered |
| ~30 s | answered |
| 133 s | lost |
| 175 s | lost |
| ~290 s | lost |
After the drop, `readyState` is still `OPEN`, so `ws.send()` succeeds and the frame goes nowhere. No `close` event fires, so partysocket's reconnect (which is driven by close events) never runs. `useAgentChat` sends `cf_agent_use_chat_request`, gets no `cf_agent_use_chat_response`, and the AI SDK sits in `submitted` forever.
## Why
Cloudflare's [WebSockets docs](https://developers.cloudflare.com/network/websockets/): "Cloudflare will close a WebSocket connection when no data is transmitted in either direction for a period of time. [...] To keep long-lived connections alive during periods of inactivity, implement a client-side heartbeat (ping/pong) mechanism."
The [Workers best-practices page](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/) gives the Durable Object half: "Use `setWebSocketAutoResponse` for ping/pong heartbeats that do not wake the object." Per the [pricing page](https://developers.cloudflare.com/durable-objects/platform/pricing/), auto-responses cost nothing.
Neither half exists in the SDK. The only "heartbeat" is the alarm-backed `keepAlive()`, which keeps the *object* in memory and never touches the socket.
Most apps do not notice because something else closes or replaces the socket first: a reload, a deploy, or a protocol with its own heartbeat. A chat pane left open until the person has a question is exactly the case that shows it.
## Proposal
1. **Server:** the lifecycle registers a `ping` → `pong` auto-response pair in the constructor, by default or as an option, the way `cloudflare/actors` does it (`config.sockets.autoResponse`, `packages/core/src/index.ts`). Think and the base `Agent` already drop non-protocol text frames silently, so today a `ping` that reaches `onMessage` wakes the object and is ignored.
2. **Client:** `AgentClient` (and so `useAgent`) sends `ping` on a configurable interval while open, and treats a missing `pong` as a dead socket: close locally so the existing reconnect path runs. The client and `useAgentChat` already ignore non-JSON frames, so `pong` needs no handling beyond the liveness check.
Apps can add both around the SDK today (we are), but every Agent socket is exposed to the same rule, and only the SDK can fix it once.
## References
- [`setWebSocketAutoResponse`](https://developers.cloudflare.com/durable-objects/api/state/#setwebsocketautoresponse)
- [Hibernation server example](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/), which sets the pair in the constructor
Contributor guide
Assessment
This issue has not been assessed yet.