TokenPAPATokenPAPA
User GuideAPI ReferenceAI ApplicationsBlog

SSE Streaming with LLMs: A Practical From-Zero Guide

SSE (Server-Sent Events) streaming with LLM APIs from zero: how token streaming works on the wire, fetch vs EventSource, SSE vs WebSocket, common pitfalls, and working code.

SSE Streaming with LLMs: A Practical From-Zero Guide

If you have ever watched a chat interface type out an answer token by token, you were looking at SSE (Server-Sent Events) in action. Streaming is not a nice-to-have anymore: users expect the first token in a few hundred milliseconds, not after a 10-second spinner. And with an OpenAI-compatible API aggregator such as TokenPAPA, the same streaming code works for DeepSeek, GPT-5.6, Qwen and Claude alike.

This guide goes from zero to production: what SSE is, how an LLM stream looks on the wire, how to consume it in Python and in the browser, when to pick SSE over WebSocket, and the pitfalls that waste real engineering time.


What It Costs (per 1M tokens)

Streaming changes the feel of a model, but it does not change the meter: you still pay per token. Here is what the budget-friendly tier looks like in 2026:

ModelInput /1MOutput /1MBest for
DeepSeek V4 Flash$0.14$0.42Default streaming assistant
DeepSeek V4 Pro$0.28$0.84Harder reasoning, still fast
GPT-5.6 Luna$0.27$2.70OpenAI-ecosystem apps
GPT-5.6 Sol$13.50$60.00Frontier quality, high budget

DeepSeek V4 Flash input is 96% cheaper than GPT-5.6 Sol — and with a time-to-first-token around 0.4s it streams just as snappily. A simulated production workload of 100K requests/month runs about $52/month on V4 Flash versus $4,200/month on the flagship tier. Cheap models make streaming architecture affordable at scale.


What SSE Actually Is

SSE is a one-way, HTTP-based push protocol. The client opens a normal HTTP request; the server keeps the connection open and writes text/event-stream lines whenever it has data:

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"}}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":" world"}}]}

data: [DONE]

Each data: line is one event; a blank line terminates it. Because it rides on plain HTTP, SSE works through firewalls, load balancers, and standard libraries — no special protocol handshake, no persistent connection pool of its own. LLM providers use exactly this format for OpenAI-compatible streaming.


Streaming from Python

The OpenAI SDK hides the wire format. Set stream=True and iterate over deltas:

from openai import OpenAI

client = OpenAI(
    api_key="YOUR_TOKENPAPA_KEY",
    base_url="https://tokenpapa.ai/v1"
)

stream = client.chat.completions.create(
    model="deepseek-v4-flash",
    max_tokens=512,
    stream=True,  # SSE under the hood
    messages=[{"role": "user", "content": "Explain SSE in one paragraph."}],
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

One important habit: always set max_tokens. Output tokens cost 3–10x input on most models, and an interrupted stream that already generated 4,000 tokens still bills for all of them.

The same stream=True code works against any OpenAI-compatible API aggregator — switch model to gpt-5.6-luna or qwen3.7-plus and nothing else changes.


Streaming in the Browser: fetch, Not EventSource

The naive approach is EventSource, the built-in SSE client:

const es = new EventSource('/api/chat/stream');
es.onmessage = (e) => { console.log(e.data); };

It auto-reconnects and is trivial — but two problems make it wrong for most LLM apps:

  1. EventSource cannot set custom headers. No Authorization header, no custom model param without URL hacks.
  2. You should not put an API key in the browser anyway. The key must live server-side.

The robust pattern is a thin backend proxy that holds the key, plus a fetch-based parser in the browser. First, the backend streams from the API with the OpenAI SDK (exactly the Python code above), exposing it at /api/chat. Then the frontend parses SSE from a plain fetch response:

const resp = await fetch('/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ prompt: 'Explain SSE briefly.' }),
});

const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  // Split on blank lines (event terminator)
  const events = buffer.split('\n\n');
  buffer = events.pop() ?? '';

  for (const raw of events) {
    const line = raw.split('\n').find(l => l.startsWith('data:'));
    if (!line) continue;
    const data = line.slice(5).trim();
    if (data === '[DONE]') return;
    const json = JSON.parse(data);
    const text = json.choices?.[0]?.delta?.content;
    if (text) appendToUI(text); // typewriter effect
  }
}

That is a complete streaming chat loop — no SDK required in the browser.


SSE vs WebSocket: Which One to Use?

DimensionSSEWebSocket
DirectionServer → client (one-way)Bidirectional
ProtocolPlain HTTP (text/event-stream)Own handshake (ws://)
Auto-reconnectBuilt inYou implement it
Headers/authLimited in EventSource; use fetch/proxyFull control
Best forLLM token streams, notificationsChat rooms, gaming, collaborative editing

For LLM chat you send one prompt and receive a stream of tokens: that is a textbook one-way push. SSE gives you auto-reconnect, HTTP/2 multiplexing, and no extra server state for free. WebSocket is the right tool only when the server must initiate messages at arbitrary times — a live cursor, a multiplayer board, a trading ticker. Many teams run both: WebSocket for presence, SSE for model output.


Common Pitfalls (and Fixes)

1. Proxy buffering kills the stream. Nginx and some CDNs buffer responses by default, so tokens arrive in one giant blob — or the connection times out. Disable buffering on the streaming route: proxy_buffering off; and send the X-Accel-Buffering: no header. Test behind every proxy you deploy to.

2. Client timeouts. LLMs can pause between tokens longer than a default 30s HTTP timeout, especially on reasoning models. Set a generous read timeout (60s+) or use a stream-friendly client, and distinguish "no bytes at all" (real timeout) from "bytes then a pause" (normal).

3. Forgetting the [DONE] sentinel. Some parsers treat the final data: [DONE] line as JSON and crash. Check for it before calling JSON.parse.

4. Buffering partial events. A token chunk can arrive split across two network reads — or two chunks in one read. Always accumulate into a buffer and split on \n\n, as the browser example above does.

5. Backpressure in Python. If you consume a stream slower than the provider sends it, memory grows. Iterate and process deltas promptly, or use the SDK's async client for concurrent streams.


FAQ

What is SSE in LLM streaming? SSE (Server-Sent Events) is an HTTP-based protocol where the server pushes events over one long-lived connection. LLM APIs use it to send each token as soon as it is generated, so the first token can arrive in about 0.4s instead of waiting for the full response.

SSE vs WebSocket: which should I use for LLM streaming? For chat completions, use SSE: one request in, a one-way token stream out, with auto-reconnect built in. Use WebSocket when the server must push unsolicited messages anytime, such as collaborative editing or live dashboards.

Why can't EventSource send an Authorization header to my LLM API? The EventSource API cannot set custom headers, and API keys should never live in the browser anyway. Use a small backend proxy that holds the key and forwards the stream, or a fetch-based parser over a ReadableStream.

How do I stream tokens from an OpenAI-compatible API? Set stream: true in the chat completions request. The OpenAI SDK yields delta chunks, and the raw response is a text/event-stream where each data: line is a JSON chunk until the final [DONE] marker. An OpenAI-compatible API aggregator such as TokenPAPA supports this out of the box.


Get Started with TokenPAPA

  1. Sign up at tokenpapa.ai$1 free credit, email only, no Chinese phone number.
  2. Create an API key.
  3. Stream 30+ models through one OpenAI-compatible endpoint:
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_TOKENPAPA_KEY",
    base_url="https://tokenpapa.ai/v1"
)

stream = client.chat.completions.create(
    model="deepseek-v4-flash",
    max_tokens=256,
    stream=True,
    messages=[{"role": "user", "content": "Stream this answer to me."}],
)

for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="", flush=True)

One key, 30+ models, token-by-token. That is streaming the way it should feel.

How is this guide?

Last updated on

SSE Streaming with LLMs: A Practical From-Zero Guide | TokenPAPA