Push From Server to Client — Without WebSockets
Server-Sent Events
SSE keeps a persistent HTTP connection open and streams events. The right tool for AI streaming, live tickers, log tails.
What you'll learn
- Implement an SSE endpoint
- Consume with EventSource
- Compare with WebSockets
Server-Sent Events (SSE) keep a single HTTP connection open and
stream data: ... events to the browser. One-way (server →
client), auto-reconnect, works through proxies.
It’s what most AI streaming UIs use to push tokens.
The Server
app.get("/events", (req, res) => {
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"connection": "keep-alive",
});
// initial event
res.write(`data: ${JSON.stringify({ type: "hello" })}\n\n`);
// periodic events
const id = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
// clean up when client disconnects
req.on("close", () => clearInterval(id));
}); The format is strict: data: <text>\n\n.
The Client
const es = new EventSource("/events");
es.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log("got", data);
};
es.onerror = () => {
console.error("disconnected — browser will retry automatically");
}; EventSource auto-reconnects when the connection drops.
Named Events
event: tick
data: {"value":42}
event: alarm
data: {"severity":"high"} res.write(`event: tick\ndata: ${JSON.stringify({ value: 42 })}\n\n`);
res.write(`event: alarm\ndata: ${JSON.stringify({ severity: "high" })}\n\n`); Client listens by event name:
es.addEventListener("tick", (e) => { /* ... */ });
es.addEventListener("alarm", (e) => { /* ... */ }); AI Streaming Pattern
app.post("/chat", async (req, res) => {
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"connection": "keep-alive",
});
for await (const chunk of llmClient.stream(req.body.prompt)) {
res.write(`data: ${JSON.stringify({ delta: chunk })}\n\n`);
}
res.write("data: [DONE]\n\n");
res.end();
}); Token-by-token UI without WebSockets.
SSE vs WebSockets
| SSE | WebSocket | |
|---|---|---|
| Direction | Server → Client | Both ways |
| Protocol | HTTP | Custom (upgraded from HTTP) |
| Reconnect | Built-in | Roll your own |
| Proxies/firewalls | Friendly | Sometimes blocked |
| Browser support | Universal (no IE) | Universal |
| Format | Text only | Text or binary |
If the client doesn’t need to send (or sends rarely via plain POST), SSE is simpler. WebSockets only when bidirectional message-rate matters.
Behind A Reverse Proxy
Nginx buffers responses by default — SSE breaks until you disable:
location /events {
proxy_pass http://app;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
} The same applies behind Cloudflare and most other proxies — check the docs.
WebSockets →