> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connection Lifecycle & Limits

> How WebSocket connections to a Stateful Actor begin and end: the handshake guarantees, the close-code table, today's duration budget, a reconnect pattern, and the frame, queue, and handler limits.

<Info>
  WebSocket support for Stateful Actors is in **beta**. APIs and limits may change before general availability.
</Info>

One contract underlies everything on this page: **every connection ends**. A redeploy severs it, the duration budget expires it, a failing handler closes it. Build for that from the start — treat the socket as disposable transport and the actor as the durable thing. Reconnecting is the client's job; `idFromName` with the same name routes the new socket back to the same actor, and everything in `ctx.storage` is exactly where the last connection left it. A client that reconnects with backoff and re-syncs on open loses nothing but the socket.

## How a Connection Is Established

Your function's `fetch` runs **once**, at the handshake — check the `Upgrade` header, authenticate, pick the actor, forward. It never sees another byte of the connection; per-message logic lives in the actor's `webSocket()` handler. The full front-door pattern is on the [WebSockets](/docs/edge-compute/stateful-actors/websockets) page.

What a client can rely on at and after the handshake:

* **The upgrade must be a `GET`.** A non-`GET` request with an `Upgrade` header is refused with HTTP `400` — no handshake starts.
* **The first client-offered subprotocol is echoed.** Offer `["chat.v2", "chat.v1"]` and the connection is accepted with `chat.v2`. The actor cannot select among the offers today; it observes the echoed value as `ws.protocol`. Put your preferred protocol first.
* **Compression is never negotiated.** A `permessage-deflate` offer is accepted without the extension; frames flow uncompressed.
* **Text and binary frames both work.** The handler's `message` listener receives `(data, isBinary)`; binary frames round-trip byte-identically.
* **Protocol-level pings are answered by the platform**, payload echoed. You don't write keepalive code on the actor side.

## How Connections End

| Code          | Meaning                                                                                                                                                                                                               | What the client should do                                                                                                                                                                                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `1000`        | Normal close — either side asked for it (`ws.close(1000, ...)` on the actor, or the client).                                                                                                                          | Nothing. Don't reconnect.                                                                                                                                                                                                                                                                 |
| `1005`        | No close code received — a bare `ws.close()` or browser close with no code argument surfaces as `1005` at the peer. Treat as deliberate if you control both sides; otherwise indistinguishable from an abnormal drop. | Reconnect if the close was not expected; otherwise no action.                                                                                                                                                                                                                             |
| `1001`        | Actor going away — graceful drain before a restart or move. Graceful drain is still rolling out; today an actor-side restart can surface as `1006` instead.                                                           | Reconnect promptly; the actor comes back under the same name.                                                                                                                                                                                                                             |
| `1006`        | Abnormal — the connection was severed with no close frame: the duration budget expired, a function was redeployed, or infrastructure restarted.                                                                       | Reconnect with backoff.                                                                                                                                                                                                                                                                   |
| `1009`        | An inbound frame exceeded the 1 MiB cap.                                                                                                                                                                              | Fix the client — chunk large payloads. Reconnecting without fixing it hits the same wall.                                                                                                                                                                                                 |
| `1011`        | A message handler threw or exceeded the 30-second method budget. Also sent when the actor class has no `webSocket()` handler at all (reason: `actor has no webSocket handler`).                                       | Reconnect, but treat repeats as a server-side bug to fix, not a transient.                                                                                                                                                                                                                |
| `1013`        | Event queue overflow — frames arrived faster than handlers drained them (256 events / 1 MiB queued). The actor is overloaded.                                                                                         | Back off **longer** than for `1006` before reconnecting, and slow your send rate. High-frequency clients should [batch messages into fewer frames](/docs/edge-compute/stateful-actors/websockets#messages-are-single-threaded-like-everything-else) — the queue counts events, not bytes. |
| `4000`–`4999` | Application-defined — the RFC 6455 private-use range. The platform doesn't use these codes; one of them means your own actor called `ws.close()`, and the code and reason arrive exactly as the actor sent them.      | Whatever your protocol assigned it — kicked, room closed, session expired. It's a deliberate close: act on it, don't blind-retry it.                                                                                                                                                      |

**Today, a connection lives at most about five minutes.** The total-duration budget applies even to a socket actively exchanging frames; the cutoff surfaces as close code `1006` with no close frame. This is a current limit, not the contract — it will be raised in a later release. Until then, design clients to ride through a reconnect every few minutes as a matter of course.

There is no separate idle kill below that budget — a socket that goes quiet for 90 seconds and then sends a frame still gets that frame delivered and handled. The duration budget is what ends it.

That guarantee covers the platform's half of the path only. NATs, corporate proxies, and load balancers between your client and Telnyx run their own idle timers, and any of them can drop a quiet connection long before the duration budget would — often silently, leaving the client holding a socket that looks open but delivers nothing. No close event fires for a drop like that, so reconnect logic alone never notices it; only traffic does. If your clients sit quiet for long stretches, send a periodic heartbeat: non-browser clients can use a protocol-level ping, which the platform answers, and browser `WebSocket` clients — which cannot send pings — should use a small application-level frame the `webSocket()` handler replies to. A heartbeat frame is one more frame through the same single-threaded dispatch, so keep it small and infrequent, and give your client library's own idle or read timeouts the same review.

**Deploys sever live sockets.** Shipping a new version of your function drops every connection it carries as `1006`. This is another reason reconnect logic is not optional: your own deploys exercise it.

## Reconnecting

A complete client. It reconnects on every close except the deliberate ones — `1000`, or an application-defined `4xxx` — backs off exponentially, backs off harder on `1013`, and re-syncs on open — the socket is new even though the actor and its storage are not.

```ts theme={null}
function connect(room: string, attempt = 0): void {
  const ws = new WebSocket(
    `wss://<your-function-host>/rooms/${room}?user=me`,
  );
  let openedAt = 0;

  ws.onopen = () => {
    openedAt = Date.now();
    // Re-request whatever context this client needs. Durable state picked
    // up where it left off — a counter in ctx.storage that read 2 before
    // the drop reads 3 after the next message, not 1.
    ws.send(JSON.stringify({ op: "sync" }));
  };

  ws.onmessage = (ev) => {
    // ... application protocol ...
  };

  ws.onclose = (ev) => {
    if (ev.code === 1000) return; // done on purpose
    if (ev.code >= 4000) return; // app-defined — deliberate; act on it, don't retry it
    // Reset the backoff only after a connection proved stable — an actor
    // that accepts the handshake and then dies must still back off.
    if (openedAt > 0 && Date.now() - openedAt > 30_000) attempt = 0;
    const base = ev.code === 1013 ? 2_000 : 250; // overloaded → back off harder
    const delay = Math.min(base * 2 ** attempt, 30_000) + Math.random() * 250;
    setTimeout(() => connect(room, attempt + 1), delay);
  };
}

connect("standup");
```

Reconnect to the **same name**. `idFromName("standup")` routes to the same actor every time, so the durable sequence counters, history, and alarms of the conversation are all still there. What does not survive is anything held only in the actor's instance fields or in the old socket itself — the same rule as the rest of the platform: only `ctx.storage` is real. See [Lifecycle & placement](/docs/edge-compute/stateful-actors/concepts/lifecycle).

## Limits

| Limit                                | Value                                                                                                                 | On violation                  |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| Inbound frame size                   | 1 MiB                                                                                                                 | Close `1009`                  |
| Event queue while a handler runs     | 256 events / 1 MiB                                                                                                    | Close `1013`                  |
| Per-message method budget            | 30 seconds                                                                                                            | Close `1011`                  |
| Connection duration                  | \~5 minutes today (expected to increase)                                                                              | Close `1006`, no close frame  |
| Socket scope                         | One actor id — `ctx.count()` and `ctx.broadcast()` reach only this id's sockets; sockets never carry over between ids | —                             |
| Actor names used with sockets        | Printable ASCII, no leading/trailing whitespace, today                                                                | Connection fails to establish |
| Header values your function forwards | Printable ASCII today                                                                                                 | Connection fails to establish |
| Outbound frame size                  | No platform cap — unsent bytes wait in the actor's memory until the client's connection drains them                   | —                             |

The queue limit is a consequence of single-threaded dispatch: an inbound frame is handled like a method call — one at a time per instance, serialized with RPCs and alarms. While one handler runs, further frames queue; a slow handler with fast senders overflows the queue and the socket closes `1013`. Keep per-message work well under the 30-second budget — the queue bound is sized for handlers that finish in milliseconds, not tens of seconds.

The same serialization sets the practical ceiling on how many sockets one instance should carry. The table has no socket-count row because the working bound is not a count — it is aggregate frame rate. An instance handles one frame at a time from all of its sockets combined: at one millisecond of handler work per frame, the whole instance drains at most on the order of a thousand inbound frames per second, and a `ctx.broadcast()` inside a handler writes one frame to every connected socket, so fan-out work grows with the audience. The 256-event queue absorbs a burst of at most a couple hundred frames, not thousands of senders talking at once. When one name's combined rate approaches what its handlers can drain, partition the workload across names — a room per `idFromName`, a shard per topic or region — so each instance coordinates only the sockets whose combined traffic it can serialize. An actor is a unit of coordination, not a unit of capacity.

The outbound direction is the mirror image, and it is not policed. `ws.send()` never blocks the handler, and the platform never drops or coalesces frames on a live connection — anything the client hasn't drained yet waits in the actor's memory, and `ws.bufferedAmount` (standard Node `ws`) reports how much is queued on a socket. Against a responsive client that number stays near zero; against a stalled one — a backgrounded tab, a dead radio link — every `ctx.broadcast()` tick adds another payload to the stalled socket's queue. Bound what one slow consumer can cost: keep broadcast payloads small, and when updates outpace a client, send the latest state rather than queueing every delta — durable state in `ctx.storage` means a client that fell behind can re-sync from a snapshot instead of replaying a backlog. A socket whose `bufferedAmount` only grows is gone in every way that matters; close it, and the reconnect-and-re-sync logic every client here needs anyway will pick it back up.

## What Holds the Actor in Memory

While a connection is open its actor stays resident — the actor is not deactivated between frames — and under today's duration budget a connection never lives long enough for the idle eviction described in [Lifecycle & placement](/docs/edge-compute/stateful-actors/concepts/lifecycle) to be a factor. The cost model follows: a million idle actors are cheap; a million actors each holding an open socket are a million resident activations.

Teardown is orderly on the actor side. `close` and `error` listeners run under the same single-threaded dispatch as any other event, and a `ctx.storage` write inside a `close` handler commits — the instance stays resident until those final handlers finish, not merely until the raw socket drops. Recording "last seen" in a close handler is safe. After the last socket's close handler finishes, the actor is an ordinary idle actor again: evictable, and revivable by the next `idFromName` call — or by an [alarm](/docs/edge-compute/stateful-actors/alarms) it set while the connection was up.

## Next Steps

* [WebSockets](/docs/edge-compute/stateful-actors/websockets) — the front-door pattern, the `webSocket()` handler, and how messages dispatch
* [Lifecycle & placement](/docs/edge-compute/stateful-actors/concepts/lifecycle) — eviction and restart for actors generally
* [Alarms](/docs/edge-compute/stateful-actors/alarms) — deferred work that outlives any connection
