Saturday, January 24, 2026

SSE vs HTTP Streaming, for AI MCP

  • Streaming HTTP: Unidirectional (server-to-client). The server sends data chunks, but the client can’t respond over the same connection.
  • WebSocket: Bidirectional. Both client and server can send messages at any time, ideal for interactive use cases.
  • SSE: Unidirectional (server-to-client). The server pushes events, but clients need separate requests for sending data.



Server-Sent Events (SSE) is a specific, standardized API built on top of generic HTTP streaming that provides a robust, simple way to push data from a server to a client over a single HTTP connection. HTTP streaming is a broader, lower-level technique that lacks the built-in features of SSE. 
Key Differences
Feature HTTP Streaming (Generic)Server-Sent Events (SSE)
DefinitionA general technique where the server sends data in chunks over an open HTTP connection.A specific standard that uses text/event-stream MIME type to deliver structured, event-based text data.
APIRequires manual handling of data chunks and parsing, often using the low-level fetch() with readable streams.Uses the dedicated, high-level EventSource browser API which simplifies client-side implementation.
ReconnectionRequires custom logic to manage disconnections and reconnections.Automatic reconnection is handled by the browser's EventSource API out-of-the-box.
Data FormatCan stream various formats (JSON, plain text, binary) using methods like chunked transfer encoding or NDJSON.Limited to text-based data in a specific line-based format (data: message\n\n).
Event TypesData is unstructured; application-level logic needed to differentiate message types.Supports named events, allowing different client-side handlers for various event types (e.g., 'ping''message').
ComplexityMore complex to implement reliably due to the need to handle low-level connection management and parsing.Simpler to implement for unidirectional flow due to built-in browser features and standard HTTP usage.
Summary
  • Choose SSE when you only need unidirectional server-to-client communication of real-time, text-based data (e.g., live news feeds, stock tickers, notifications, AI responses). Its simplicity and built-in browser features make it highly effective for these use cases.
  • Use generic HTTP streaming for more specialized scenarios, such as streaming large binary files (e.g., video chunks via HLS/DASH, as SSE is text-only) or when working in non-browser environments where the EventSource API isn't available. 
SSE builds upon HTTP streaming to provide a superior, standardized experience with features like automatic reconnection, making it the preferred method for most server-to-client event streaming needs. 


SSE (Server-Sent Events) and HTTP streaming are related but distinct approaches to pushing data from server to client.

SSE is a standardized protocol built on top of HTTP streaming. It uses a specific format with text/event-stream content type, where messages follow a structured pattern: data:, event:, id:, and retry: fields separated by newlines. The browser's EventSource API handles reconnection automatically, tracks the last event ID, and resumes where it left off. It's unidirectional (server to client only) and works over a single long-lived HTTP connection.

HTTP streaming is the broader technique of sending a chunked response where the connection stays open and data arrives incrementally. This could be raw bytes, newline-delimited JSON, or any format you choose. You handle it manually with fetch and reading the response body as a stream. There's no built-in reconnection logic or event structure—you implement whatever protocol suits your needs.

Practical differences:

AspectSSERaw HTTP Streaming
FormatStandardized event formatAny format you want
Browser APIEventSource with auto-reconnectManual fetch + ReadableStream
BidirectionalNo (use separate requests)No (same limitation)
Binary dataText only (base64 encode if needed)Native binary support
Event typesBuilt-in event: field routingRoll your own

SSE

TypeScript (Express)

typescript
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const interval = setInterval(() => {
    res.write(`event: message\n`);
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

Go

go
func events(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    flusher := w.(http.Flusher)
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-r.Context().Done():
            return
        case t := <-ticker.C:
            fmt.Fprintf(w, "event: message\ndata: %d\n\n", t.UnixMilli())
            flusher.Flush()
        }
    }
}

Python (FastAPI)

python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio, json

app = FastAPI()

async def event_generator():
    while True:
        yield f"event: message\ndata: {json.dumps({'time': time.time()})}\n\n"
        await asyncio.sleep(1)

@app.get("/events")
async def events():
    return StreamingResponse(event_generator(), media_type="text/event-stream")

C# (ASP.NET Core)

csharp
app.MapGet("/events", async (HttpContext ctx, CancellationToken ct) =>
{
    ctx.Response.Headers["Content-Type"] = "text/event-stream";
    ctx.Response.Headers["Cache-Control"] = "no-cache";

    while (!ct.IsCancellationRequested)
    {
        await ctx.Response.WriteAsync($"event: message\ndata: {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}\n\n", ct);
        await ctx.Response.Body.FlushAsync(ct);
        await Task.Delay(1000, ct);
    }
});

HTTP Streaming (raw chunked)

TypeScript (Express)

typescript
app.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'application/x-ndjson');
  res.setHeader('Transfer-Encoding', 'chunked');

  const interval = setInterval(() => {
    res.write(JSON.stringify({ time: Date.now() }) + '\n');
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

Go

go
func stream(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/x-ndjson")
    flusher := w.(http.Flusher)
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-r.Context().Done():
            return
        case t := <-ticker.C:
            fmt.Fprintf(w, `{"time":%d}`+"\n", t.UnixMilli())
            flusher.Flush()
        }
    }
}

Python (FastAPI)

python
async def stream_generator():
    while True:
        yield json.dumps({"time": time.time()}) + "\n"
        await asyncio.sleep(1)

@app.get("/stream")
async def stream():
    return StreamingResponse(stream_generator(), media_type="application/x-ndjson")

C# (ASP.NET Core)

csharp
app.MapGet("/stream", async (HttpContext ctx, CancellationToken ct) =>
{
    ctx.Response.Headers["Content-Type"] = "application/x-ndjson";

    while (!ct.IsCancellationRequested)
    {
        var json = JsonSerializer.Serialize(new { time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
        await ctx.Response.WriteAsync(json + "\n", ct);
        await ctx.Response.Body.FlushAsync(ct);
        await Task.Delay(1000, ct);
    }
});

Client (browser)

SSE

typescript
const source = new EventSource('/events');
source.addEventListener('message', (e) => console.log(JSON.parse(e.data)));
source.onerror = () => console.log('reconnecting...');

HTTP Streaming

typescript
const res = await fetch('/stream');
const reader = res.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const lines = decoder.decode(value).trim().split('\n');
  lines.forEach(line => console.log(JSON.parse(line)));
}

why is MCP now using HTTP streaming instead of SSE?

3:29 PM

MCP switched from SSE to HTTP streaming (specifically, streamable HTTP) primarily because SSE is unidirectional.

With SSE, the client opens a connection and the server pushes events—but the client can't send messages back over that same connection. MCP needed bidirectional communication for the full request/response pattern between client and server (tool calls, responses, progress notifications going both ways). The original SSE-based transport required two separate channels: SSE for server-to-client and regular HTTP POST for client-to-server. This introduced complexity around correlating messages, handling connection lifecycle across two channels, and dealing with reconnection edge cases.

The new streamable HTTP transport uses a single POST request where both the request body and response body can be streamed. The client streams JSON-RPC messages to the server, and the server streams responses back—true bidirectional over one connection. This simplifies connection management, eliminates the need to synchronize two channels, and maps more cleanly onto the JSON-RPC semantics MCP uses.

There were also pragmatic concerns: SSE has inconsistent proxy/load balancer support, some infrastructure drops long-lived connections, and EventSource's automatic reconnection behavior could conflict with application-level session management. A standard HTTP POST with streaming bodies works more reliably across diverse deployment environments.

No comments: