> ## 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.

# WebSockets

> Agents terminate live WebSockets today through the actor surface: authenticate at the handshake, handle frames on the agent, broadcast replies from tasks.

An `Agent` can terminate WebSockets **today**. The capability comes from
[`StatefulActor`](/docs/edge-compute/stateful-actors/websockets): define one method,
`webSocket()`, and the agent owns the live socket — frames dispatch one at a time,
serialized with RPCs and tasks like every other call, with history, state, and timers
right there in the handler.

<Note>
  Actor WebSocket support is in **beta** — the platform guide carries the full contract
  and current caveats.
</Note>

## The shape

A function terminates the handshake — authenticate once, pick the agent, hand off — and
the agent handles every frame after that:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";
import type { ActorNamespace } from "@telnyx/edge-runtime";
import type { WebSocket } from "ws"; // transitive dependency of @telnyx/edge-runtime

export class Assistant extends Agent {
  // req carries whatever headers your front door stamped at the handshake
  async webSocket(ws: WebSocket, req: Request): Promise<void> {
    ws.on("message", async (data, isBinary) => {
      if (isBinary) return;
      await this.messages.add("user", String(data));
      await this.queue("respond"); // think in the background
    });
  }

  async respond(): Promise<void> {
    const history = await this.messages.toOpenAI();
    // ... call your LLM here — see Calling LLMs ...
    const reply = "On it.";
    await this.messages.add("assistant", reply);
    // Fan out to every socket open on this agent
    this.ctx.broadcast(JSON.stringify({ role: "assistant", text: reply }));
  }
}

interface Env {
  ASSISTANT: ActorNamespace;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (req.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
      return new Response("expected websocket", { status: 426 });
    }
    // Authenticate here — this runs once per connection. Stamp what you learn
    // as headers for the actor; the actor WebSockets guide shows the pattern.
    // ⚠️ Derive the actor name from a verified credential (JWT, session cookie),
    // not a caller-controlled query param. This example uses ?user= for brevity.
    const user = new URL(req.url).searchParams.get("user");
    if (!user) return new Response(null, { status: 401 });
    return env.ASSISTANT.idFromName(user).fetch(req);
  },
};
```

The frame handler follows the same rule as the webhook path in the
[Quickstart](/docs/agent-sdk/quickstart): append to history, `queue()` the LLM turn,
return. A message handler runs under the **30-second method budget**, and while it runs
every other frame, RPC, and task on this agent waits — so `respond()` does the slow
thinking in a task, then pushes the reply to every open socket with
`this.ctx.broadcast()`.

## Pushing without an inbound frame

Tasks run inside the agent, so a timer can push. Use the scheduler — **not** raw
alarms, which the SDK [reserves](/docs/agent-sdk/api-reference/agent) for the task
queue:

```ts theme={null}
async webSocket(ws: WebSocket, req: Request): Promise<void> {
  // First socket arms the loop; the stable id makes re-arming an upsert
  await this.every(30, "tick", null, { id: "tick" });
}

async tick(): Promise<void> {
  if (this.ctx.count() === 0) {
    await this.cancelSchedule("tick"); // no listeners — stand down
    return;
  }
  this.ctx.broadcast(JSON.stringify({ type: "status", ...(await this.getState()) }));
}
```

## What to know before shipping

* **Connections are capped at about five minutes today**, measured from the handshake,
  even on a socket actively exchanging frames — surfacing as an abnormal close
  (code `1006`). The cap will be raised in a future update. Reconnecting is the
  client's job: back off and reopen the same name. `idFromName` routes the new socket
  to the same agent, where history, state, and pending tasks all survived — only the
  socket is new.
* **Sends aren't held for durability.** `ws.send()` and `broadcast()` are immediate; a
  handler that sends and then throws leaves the client holding a frame about state that
  never committed. Frames that carry the message log's `seq` let a client detect gaps
  and re-derive on reconnect.
* **Bound per-frame work.** Frames that arrive while a handler runs queue up to 256
  events or 1 MiB; overflow closes the socket with `1013`. Batch chatty clients into
  fewer, larger frames.

Close codes, reconnect strategy, and the full contract:
[actor WebSockets](/docs/edge-compute/stateful-actors/websockets) and
[Connection Lifecycle](/docs/edge-compute/stateful-actors/websockets/lifecycle).

## What's in development

This page is the platform surface, and it stays. In development on top of it: `AgentClient`,
a browser SDK — browser clients connecting straight to an agent, automatic reconnect, and
state sync driven by [`onStateChanged`](/docs/agent-sdk/api-reference/state) — with
`onConnect()` on the `Agent` class as the seam it lands on. Until then, `webSocket()` is
the supported path.
