Choosing the Right Protocol for AI Streaming: SSE, Chunked, or WebSocket (Part 1)

This article analyzes three HTTP‑based streaming approaches—Server‑Sent Events (SSE), HTTP Chunked Transfer Encoding, and WebSocket—detailing their underlying mechanisms, implementation steps, pros and cons, and ideal use cases to help developers select the most suitable protocol for AI‑driven real‑time output.

AI Engineer Programming
AI Engineer Programming
AI Engineer Programming
Choosing the Right Protocol for AI Streaming: SSE, Chunked, or WebSocket (Part 1)

Streaming options for AI output

In AI services the server often needs to send incremental data to the client. Three HTTP‑based mechanisms are compared: Server‑Sent Events (SSE), HTTP Chunked Transfer Encoding, and WebSocket.

1. Server‑Sent Events (SSE)

What it is

SSE is a lightweight server‑push technology defined by the WHATWG HTML Living Standard. It streams plain‑text events over an HTTP connection with Content‑Type: text/event-stream.

Mechanics

Connection keep‑alive – relies on the HTTP keep‑alive header.

Automatic reconnection – browsers reconnect after the interval specified by the retry field.

Resume support – if the server sends an id field the browser stores it and resends Last‑Event‑ID on reconnect.

HTTP/2 multiplexing – multiple SSE streams can share a single TCP connection (limited by SETTINGS_MAX_CONCURRENT_STREAMS = 100).

Limitations of the native EventSource

The built‑in EventSource API only supports GET, cannot set custom headers (e.g., Authorization) and cannot send a JSON body. OpenAI works around this by using fetch + ReadableStream to parse the SSE format, enabling POST with a JSON payload.

Reference specifications

WHATWG HTML Living Standard – https://html.spec.whatwg.org/multipage/server-sent-events.html<br/>MDN Server‑sent events – https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events

Python FastAPI example

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio, json, time

app = FastAPI()

@app.get("/stream")
async def sse_stream(request: Request):
    async def event_generator():
        count = 0
        while True:
            if await request.is_disconnected():
                break
            yield f"id: {count}
"
            payload = {"time": time.time(), "count": count}
            yield f"data: {json.dumps(payload)}

"
            count += 1
            await asyncio.sleep(1)
    return StreamingResponse(event_generator(),
        media_type="text/event-stream",
        headers={"Cache-Control":"no-cache","Connection":"keep-alive","X-Accel-Buffering":"no"})

JavaScript client example

const source = new EventSource('/stream');
source.onmessage = e => {
    const data = JSON.parse(e.data);
    console.log('Received:', data);
};
source.onerror = e => {
    if (e.target.readyState === EventSource.CLOSED) {
        console.error('Connection permanently closed');
    } else {
        console.log('Reconnecting...');
    }
};

Key pitfalls

Only unidirectional (server → client).

Native EventSource limited to GET; POST requires manual parsing or a library.

Binary data must be Base64‑encoded.

HTTP/1.1 browsers limit to six concurrent connections per host.

Typical use cases

AI large‑model token streaming (OpenAI, Claude, Gemini).

Real‑time price feeds, social updates.

Log dashboards.

Call sequence:

SSE call sequence
SSE call sequence

2. HTTP Chunked Transfer Encoding

What it is

Chunked Transfer Encoding is defined in RFC 9112 §7.1 (HTTP/1.1) and RFC 9113 §6.1 (HTTP/2). It allows a server to send a response body in a series of length‑prefixed chunks when the total size is unknown. It is a transport‑layer feature, not a separate protocol.

Mechanics

Each chunk starts with its length in hexadecimal followed by \r\n, the data, and another \r\n. A zero‑length chunk terminates the response.

Can be combined with Content‑Encoding: gzip (compress then chunk).

Designed for dynamic content where the size cannot be predetermined (e.g., large queries, live logs).

Reference specifications

RFC 9112 – https://datatracker.ietf.org/doc/html/rfc9112#section-7.1<br/>RFC 9113 – https://datatracker.ietf.org/doc/html/rfc9113#section-6.1

Python Flask/FastAPI example

from flask import Flask, Response
import time

app = Flask(__name__)

@app.route('/chunked')
def chunked_download():
    def generate_chunks():
        yield "First chunk data
"
        time.sleep(1)
        yield "Second chunk data
"
        time.sleep(1)
        yield "Third chunk data
"
        time.sleep(1)
        yield "End
"
    return Response(generate_chunks(), content_type='text/plain')

JavaScript fetch client

async function fetchChunked() {
    const response = await fetch('/chunked');
    const reader = response.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});
        const lines = buffer.split('
');
        buffer = lines.pop() || '';
        for (const line of lines) {
            console.log('Current accumulated data:', line);
        }
    }
}

Key pitfalls

The read() method may return data that does not align with chunk boundaries; browsers strip the length header and CRLF, delivering only the raw payload.

No built‑in event semantics – the stream is just a sequence of bytes.

No automatic reconnection; resumable downloads require Range requests.

Typical use cases

Large file download with progressive write to disk.

Progressive image rendering (blur‑to‑sharp).

Streaming JSON where each chunk is a separate JSON object.

Video streaming in progressive chunks.

Call sequence:

Chunked Transfer Encoding call sequence
Chunked Transfer Encoding call sequence

3. WebSocket

What it is

WebSocket (RFC 6455) provides a full‑duplex communication channel over a single TCP connection. After an HTTP Upgrade handshake the protocol switches to a lightweight frame format.

Mechanics

Handshake – client sends GET /… with Upgrade: websocket and Connection: Upgrade; server replies 101 Switching Protocols and Sec‑WebSocket‑Accept.

Frame structure – each frame contains FIN, Opcode (text, binary, close, ping, pong), MASK (client‑to‑server must be set), payload length and optional masking key. Header size ranges from 2 to 14 bytes.

Ping/Pong – automatic heartbeat handled by browsers and server libraries.

Reference specifications

RFC 6455 – https://datatracker.ietf.org/doc/html/rfc6455<br/>W3C WebSocket API – https://websockets.spec.whatwg.org/

Python websockets 13+ example

import asyncio, json, websockets

async def chat_handler(websocket):
    print(f"Client connected from {websocket.remote_address}")
    try:
        async for message in websocket:
            data = json.loads(message)
            print(f"Received JSON: {data}")
            await websocket.send(json.dumps({"echo": data}))
    except websockets.ConnectionClosed:
        print("Client disconnected")

async def main():
    async with websockets.serve(chat_handler, "0.0.0.0", 8080):
        print("WebSocket server running on ws://0.0.0.0:8080")
        await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(main())

JavaScript browser client

const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => console.log('Connected');
ws.send(JSON.stringify({type:'join', room:'general'}));
ws.onmessage = event => {
    try {
        const data = JSON.parse(event.data);
        console.log('Received:', data);
    } catch {
        console.log('Received binary:', event.data);
    }
};
ws.onclose = e => console.log(`Closed: code=${e.code}`);
ws.onerror = err => console.error('Error:', err);
// simple manual reconnection
setTimeout(() => new WebSocket('wss://example.com/chat'), 3000);

Key pitfalls

Requires an HTTP Upgrade step; some legacy proxies or firewalls may block it.

Stateful connection – scaling often needs sticky sessions or shared state.

No built‑in automatic reconnection; the client must implement it.

Browser limits concurrent WebSocket connections (~255 in Chrome).

Typical use cases

Real‑time chat (Discord, web‑based WeChat).

Multiplayer online games (position sync, state broadcast).

Collaborative editing (Google Docs, Figma).

High‑frequency trading data feeds.

Call sequence:

WebSocket call sequence
WebSocket call sequence

Takeaways

One‑way push → SSE; raw byte streaming → Chunked Transfer Encoding; true bidirectional communication → WebSocket.

SSE is ideal for AI token streams and notification‑style updates. Chunked Transfer Encoding is a generic HTTP mechanism for streaming arbitrary binary data when the total size is unknown. WebSocket provides genuine full‑duplex messaging with minimal overhead.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

WebSocketSSEprotocol selectionHTTP ChunkedAI streaming
AI Engineer Programming
Written by

AI Engineer Programming

In the AI era, defining problems is often more important than solving them; here we explore AI's contradictions, boundaries, and possibilities.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.