← Blog

Claude API Streaming with WebSockets: What Works

2026-10-06 · 5 min read · SubToAPI Team

If you're searching for "Claude API streaming with WebSockets," you've probably hit the same wall most people do: Claude's API doesn't support WebSockets natively. It streams responses over HTTP using Server-Sent Events (SSE), not a persistent bidirectional WebSocket connection. That's not a limitation you need to work around with hacks — it's a deliberate design choice, and it's actually simpler to build on than WebSockets in most cases.

This article explains why Claude streams over SSE instead of WebSockets, how to consume that stream correctly, and — if your frontend architecture genuinely needs WebSockets (for a chat widget, a multiplayer app, or a existing WS-based infrastructure) — how to bridge SSE into a WebSocket connection without losing streaming performance.

Why Claude's API uses SSE, not WebSockets

SSE is a one-way streaming protocol over plain HTTP. The server keeps the connection open and pushes data: events as they're generated. For an LLM response, that maps perfectly: the client sends one prompt, the server streams tokens back until it's done, then the connection closes. There's no need for the client to send messages back mid-stream.

WebSockets are built for bidirectional, persistent communication — think multiplayer games, live collaborative editors, or chat apps where either side can push messages at any time. For a request/response pattern like "send a prompt, get a streamed completion," WebSockets add connection management overhead (ping/pong frames, reconnect logic, handshake complexity) without adding real benefit.

That's why Anthropic's API, and most LLM providers, stream via SSE on a standard HTTPS POST request with "stream": true in the body.

What a raw Claude streaming request looks like

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "stream": true,
    "messages": [{"role": "user", "content": "Explain SSE vs WebSockets briefly."}]
  }'

The response is a stream of event: / data: lines — message_start, repeated content_block_delta events carrying token chunks, and a message_stop at the end. Any HTTP client that can read a response body incrementally can consume this; no WebSocket library required.

Bridging SSE into a WebSocket connection

If your frontend is already wired for WebSockets — say you're running a chat app where a single WS connection multiplexes multiple conversations, presence, and typing indicators — you don't need to rip that out. You just need a server-side bridge: your backend holds the SSE connection to Claude, and forwards each chunk to the browser over your existing WebSocket.

Here's a minimal Node.js example using the ws library:

import { WebSocketServer } from "ws";
import fetch from "node-fetch";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (socket) => {
  socket.on("message", async (raw) => {
    const { prompt } = JSON.parse(raw.toString());

    const response = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "x-api-key": process.env.ANTHROPIC_API_KEY,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-5",
        max_tokens: 1024,
        stream: true,
        messages: [{ role: "user", content: prompt }],
      }),
    });

    for await (const chunk of response.body) {
      const text = chunk.toString();
      for (const line of text.split("\n")) {
        if (line.startsWith("data:")) {
          socket.send(line.slice(5).trim());
        }
      }
    }

    socket.send(JSON.stringify({ type: "done" }));
  });
});

The browser client just connects to your WebSocket as usual and parses the forwarded JSON events as they arrive — it never talks to Anthropic directly. This pattern also keeps your API key off the client, which you should be doing regardless of transport.

Things that break when people try this

A few recurring issues when teams build this bridge themselves:

None of this is exotic, but it's boilerplate you'll rewrite on every project unless you abstract it once.

A simpler path: let the gateway handle streaming

If you're building this bridge primarily because you want a clean streaming endpoint without re-implementing SSE parsing, reconnect handling, and key management every time, that's exactly what SubToAPI is for. It turns your existing Claude access into an HTTPS API with application keys (sub_live_...), proper SSE streaming support, and usage metadata per request — so your frontend (WebSocket-based or not) talks to a stable endpoint instead of managing raw provider connections.

const response = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    stream: true,
    messages: [{ role: "user", content: "Explain SSE vs WebSockets briefly." }],
  }),
});

You can then forward that stream into your own WebSocket layer exactly as shown above — same bridge pattern, less surface area to maintain. Details are in the streaming docs, and the quickstart covers setup end to end. Plans start with a free trial at signup; pricing is on /pricing.

Questions

Does Claude's API support native WebSocket connections? No. Claude streams responses over HTTP using Server-Sent Events (SSE) with "stream": true. You can wrap SSE in a WebSocket server-side if your frontend requires it, but Anthropic's API itself only exposes SSE.

Is SSE slower or less reliable than WebSockets for streaming? No — for one-way token streaming, SSE performs equivalently to WebSockets and has simpler reconnect semantics (standard HTTP retry) since it doesn't need a persistent bidirectional handshake.

Can I stream tool use (function calling) the same way as text? Yes, but tool call arguments arrive as input_json_delta fragments that need to be concatenated per content block before parsing as JSON — don't try to parse each delta on its own.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →