Real-Time AI Streaming with Server-Sent Events (SSE) and WebSockets

When an AI model generates an extensive response (e.g., 500 words of complex code or legal analysis), traditional synchronous HTTP requests force the user to stare at a blank screen or loading spinner for 10 to 20 seconds. This creates a terrible user experience. In contrast, Real-Time Streaming delivers tokens to the user interface the instant they are calculated by the GPU, reducing Time-to-First-Token (TTFT) to under 300 milliseconds!

Technology Comparison: SSE vs WebSockets vs HTTP Polling

Technology Communication Protocol Complexity & Reconnection Best Used For
Server-Sent Events (SSE) Unidirectional (Server → Client) over standard HTTP/1.1 or HTTP/2. Content-Type: text/event-stream. Low Complexity. Built into modern web browsers (EventSource). Automatic reconnection, fire-and-forget, lightweight. The Industry Standard for AI Text Streaming (used by ChatGPT, Claude, and Gemini interfaces).
WebSockets Full-Duplex (Bidirectional: Client ↔ Server) persistent TCP connection via ws:// or wss:// protocol. High Complexity. Requires dedicated WebSocket server, manual heartbeat pings, and reconnection logic. Real-time speech-to-speech audio streams, multi-user multiplayer canvas collaboration, gaming.

How Server-Sent Events (SSE) Works Under the Hood

  1. The client initiates an HTTP POST or GET request with Accept: text/event-stream.
  2. The server keeps the HTTP connection open, setting response headers:
    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive
  3. As the LLM emits each token chunk, the server writes structured text lines:
    data: {"token": "Artificial"}
    data: {"token": " Intelligence"}
    data: [DONE]
  4. The client reads the incoming chunks using ReadableStream and updates the UI state in real-time.

Full-Stack Code Implementation

1. Backend: Node.js (Express SSE Streamer)

Node.js Express SSE Streaming Server

// server_sse.js
const express = require('express');
const OpenAI = require('openai');
require('dotenv').config();

const app = express();
app.use(express.json());

const openai = new OpenAI();

app.post('/api/chat-stream', async (req, res) => {
    const { prompt } = req.body;

    // Set mandatory SSE Streaming Headers
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders(); // Establish stream immediately

    try {
        const stream = await openai.chat.completions.create({
            model: 'gpt-4o-mini',
            messages: [{ role: 'user', content: prompt }],
            stream: true, // Enable streaming chunk response
        });

        for await (const chunk of stream) {
            const token = chunk.choices[0]?.delta?.content || '';
            if (token) {
                // Send standard SSE event format
                res.write('data: ' + JSON.stringify({ token }) + '

');
            }
        }

        res.write('data: [DONE]

');
        res.end();
    } catch (err) {
        console.error("Streaming error:", err);
        res.write('data: ' + JSON.stringify({ error: err.message }) + '

');
        res.end();
    }
});

app.listen(3001, () => console.log("SSE Server running on port 3001"));

2. Frontend: Modern JavaScript / React Stream Consumer

Client-Side Fetch ReadableStream Reader

// client_stream.js (Browser / React)
async function streamAIResponse(userPrompt, onChunkReceived) {
    const response = await fetch('/api/chat-stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt: userPrompt })
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let fullResponse = "";

    while (true) {
        const { value, done } = await reader.read();
        if (done) break;

        const chunkText = decoder.decode(value);
        const lines = chunkText.split('
');

        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const dataStr = line.replace('data: ', '').trim();
                if (dataStr === '[DONE]') {
                    console.log("Streaming completed successfully!");
                    return fullResponse;
                }
                try {
                    const parsed = JSON.parse(dataStr);
                    if (parsed.token) {
                        fullResponse += parsed.token;
                        onChunkReceived(parsed.token, fullResponse);
                    }
                } catch (e) {
                    // Ignore partial chunks
                }
            }
        }
    }
    return fullResponse;
}

// Example usage: Update UI dynamically as tokens arrive
// streamAIResponse("Write a quick Python sort function", (token, fullText) => {
//     document.getElementById("output").innerText = fullText;
// });

Frequently Asked Questions (FAQ) - Student & Interview Review

Q1. Why is Server-Sent Events (SSE) preferred over WebSockets for AI chat streaming?

SSE operates over standard HTTP, making it simpler to implement and naturally compatible with corporate firewalls, API gateways, load balancers, and authentication cookies. Because AI text generation is inherently unidirectional (the user submits a prompt, and the AI streams back text), the bidirectional overhead of WebSockets is unnecessary.

Q2. What is Time-to-First-Token (TTFT) and why is it critical?

TTFT measures the time elapsed from when a user submits a query to when the very first generated word appears on the screen. Human perception perceives an application as instantaneous if TTFT is under 300-500ms, even if the complete generation takes 15 seconds.

Q3. How does reverse-proxy buffering (e.g., NGINX) break AI streaming?

By default, proxy servers like NGINX buffer responses before forwarding them to the client. If proxy buffering is enabled, NGINX will hold all incoming AI chunks until the entire response finishes, destroying the real-time streaming effect. It is fixed by setting the header: X-Accel-Buffering: no.

Q4. Can you stream structured JSON objects?

Yes. Using partial JSON parsers (like partial-json in Python or specialized streaming JSON libraries), frontends can parse incomplete JSON streams incrementally as tokens arrive, rendering UI components, cards, and tables before the JSON closes.

Explore Related Artificial Intelligence Tutorials & Next Steps

Broaden your foundational and advanced AI knowledge across our curriculum:

AI APIs Integration (Node/Py/PHP)

Handling basic HTTP requests and API keys securely on the backend.

Tool Calling & Structured JSON

Streaming partial JSON objects and structured tool calls in real-time.

In-Browser AI with WebGPU & ONNX

Client-side token generation loops without any server roundtrip latency.

30+ AI Practice Exercises

Code an interactive streaming chatbot in React and Node.js.

Share Share on Facebook Share on Twitter Share on LinkedIn Pin on Pinterest Share on Reddit

You may also like this!