When you build a chat UI or agent loop on top of frontier models, the transport you pick shapes everything downstream. For streaming gpt-4o claude sse websocket are the two realistic options, and they are not interchangeable: one is what the providers speak natively, the other is a wrapper you stand up yourself.
What the protocols actually do
SSE (Server-Sent Events) is a unidirectional HTTP stream. The client opens a request, the server holds the connection open and pushes text frames prefixed with data:. WebSocket is a bidirectional message protocol that begins as an HTTP Upgrade and then runs a framed TCP channel.
For token generation, the model only sends data to the client. The client sends one prompt and maybe a cancel signal. That asymmetry is the whole game.
Capabilities
SSE matches the output shape of LLM inference exactly. OpenAI’s /v1/chat/completions with stream: true and Anthropic’s /v1/messages with stream: true both return SSE. OpenAI emits data: {json}\n\n with choices[0].delta.content; Claude emits named events like event: content_block_delta followed by data: {json}. You get clean request/response scoping and, if you use EventSource, automatic reconnection (though EventSource forces GET, so most clients use fetch).
WebSocket gives you a persistent channel. You can send mid-stream cancellations, stream user audio tokens upstream, or multiplex ten conversations over one socket. But neither OpenAI nor Anthropic exposes a public WebSocket for chat completions. To use WebSocket you run a proxy that converts WS messages to upstream SSE calls.
Consuming SSE in the browser
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${KEY}` },
body: JSON.stringify({ model: 'gpt-4o', stream: true, messages })
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop()!;
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = line.slice(6);
if (payload === '[DONE]') return;
const json = JSON.parse(payload);
process.stdout.write(json.choices[0]?.delta?.content ?? '');
}
}
}
Wrapping SSE behind a WebSocket proxy
import WebSocket from 'ws';
import fetch from 'node-fetch';
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', async (raw) => {
const { model, messages } = JSON.parse(raw.toString());
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${KEY}` },
body: JSON.stringify({ model, messages, stream: true })
});
const reader = upstream.body!.getReader();
const dec = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) { ws.send('[DONE]'); break; }
ws.send(dec.decode(value, { stream: true }));
}
});
});
This adds a process, a TCP hop, and a serialization layer. It is real work that only pays off when the client genuinely needs to talk back on the same channel.
Cost model
Model token pricing is identical regardless of transport. You pay per output token to the provider, and if you use a gateway, per-token usage metering applies the same way.
SSE has zero marginal infrastructure cost beyond your existing HTTP stack. WebSocket forces you to run and scale the proxy above, pay for its compute, and absorb the bandwidth twice (provider→proxy→client). For high fan-out, that proxy becomes a line item. There is no scenario where WebSocket reduces model cost.
Latency and throughput
A cold SSE request pays one TLS handshake (reused under HTTP/2). The first token arrives after time-to-first-token (TTFT) from the model. WebSocket pays the same TLS plus an HTTP Upgrade round trip, then the proxy pays another TLS to the provider. You add at least one network hop and the proxy’s processing time.
Throughput is token-bound, not transport-bound. Both deliver tokens as fast as the model emits them. WebSocket does not make GPT-4o faster. If anything, a poorly tuned proxy with naive string concatenation will add GC pauses.
Ergonomics
SSE integrates with standard fetch, axios, or curl. Cancellation is an AbortController:
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal, ... });
// later: ctrl.abort();
Browser EventSource is GET-only, so most LLM clients use fetch with a stream reader as shown. Libraries like openai and @anthropic-ai/sdk handle parsing and surface async iterators.
WebSocket needs a client WebSocket object, a binary or text subprotocol, and heartbeat logic. If you front models with a gateway such as n4n.ai, its OpenAI-compatible endpoint streams SSE and forwards cache-control hints, so you avoid writing a WS shim unless you need bidirectional control.
Ecosystem
Every LLM provider and OpenRouter-class gateway speaks SSE. Tooling, SDKs, and server frameworks assume it. An inference gateway like n4n.ai addresses 240+ models behind one OpenAI-compatible SSE endpoint, with automatic fallback when a provider is rate-limited or degraded. Your SSE client does not change when the backend reroutes.
WebSocket has rich browser support but zero native presence in model APIs. You are on your own for framing, auth refresh, and backpressure.
Limits
SSE limits are HTTP limits: browsers cap ~6 connections per origin, proxies may timeout idle streams, and you cannot push from client after the request body closes. Provider rate limits still apply per API key.
WebSocket limits are stateful: servers must track sockets, guard against slow consumers, and implement ping/pong or die. Horizontal scaling needs a pub/sub layer. A forgotten heartbeat will get you silently disconnected by cloud load balancers.
Head-to-head summary
| Dimension | SSE | WebSocket |
|---|---|---|
| Direction | Server→client only | Bidirectional |
| Native provider support | Yes (OpenAI, Anthropic, gateways) | No, requires proxy |
| Infra cost | None beyond HTTP | Proxy compute + bandwidth |
| Added latency | None (direct) | +1 network hop minimum |
| Client complexity | Low (fetch + stream) |
Medium (socket, heartbeat) |
| Scaling | Stateless, HTTP/2 multiplex | Stateful, needs pub/sub |
| Best for | Token streaming, cancel via abort | Upstream mic, multiplex, control |
Which to choose
Single-page chat UI or agent dashboard. Use SSE. Call the provider directly with fetch and stream: true. Cancel with AbortController. This is the path of least resistance for streaming gpt-4o claude sse websocket decisions.
Backend orchestration between services. Use SSE over HTTP/2. Your service mesh already handles connection pooling. Do not stand up a WebSocket proxy to talk to a model.
Voice assistant or live interruptible session. If the browser must stream audio chunks to the server while receiving tokens, WebSocket earns its keep. Run the proxy, terminate TLS once, and pump both directions. You avoid opening a second HTTP request for mic data.
Multi-tenant terminal multiplexing. When one user watches eight model sessions at once, a single WebSocket can fan in server events more efficiently than eight SSE connections hitting browser limits. Build the proxy, but cap concurrency and use a message broker.
Prototype or script. curl -N -H "content-type: application/json" -d '{"model":"gpt-4o","stream":true,"messages":[...]}' https://api.openai.com/v1/chat/completions is SSE. No code required.
Long-lived agent with periodic client directives. If the client only needs to send a new prompt or cancel, SSE plus a fresh POST is simpler than maintaining a socket. Use AbortController for cancel.
For the vast majority of builds, SSE is the correct answer. WebSocket is a specialized tool for bidirectional or massively multiplexed frontends, and it should be adopted only when that specific shape appears. The streaming gpt-4o claude sse websocket trade-off is really “use the protocol the model speaks, unless you have a second stream to push.”