Server-sent events stream text from server to browser over an ordinary HTTP response, and the EventSource API reconnects by itself. A WebSocket upgrades the connection into a two-way channel that carries text or binary in both directions. Use SSE for feeds, notifications and progress; use WebSockets when the browser must send frequent messages back.
| Feature | Server-sent events | WebSocket |
|---|---|---|
| Direction | Server to client only | Both ways at once |
| Browser API | EventSource | WebSocket |
| Protocol | Plain HTTP, text/event-stream | HTTP Upgrade, then ws:// or wss:// |
| Data | UTF-8 text only | Text or binary |
| Reconnect | Automatic, resumes with Last-Event-ID | Write it yourself |
| Open connections on HTTP/1 | 6 per browser per domain | Separate limits, set by each browser |
Updated
Not sure which one your feature needs? Tick the statements that are true for it in the helper below. It applies the same rules as the table above and says which API fits and why.
Server-sent events fit. Nothing you ticked needs a two-way channel, and EventSource gives you reconnection and resume for free.
No library needed. The response stays open; each write is one event, and the comment line keeps idle proxies from closing it.
import { createServer } from "node:http";
let n = 0;
createServer((req, res) => {
if (req.url !== "/events") return res.writeHead(404).end();
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write("retry: 3000\n\n");
const tick = setInterval(() => {
n += 1;
res.write(`id: ${n}\nevent: price\ndata: {"value": ${100 + n}}\n\n`);
}, 2000);
const ping = setInterval(() => res.write(": keep-alive\n\n"), 20000);
req.on("close", () => { clearInterval(tick); clearInterval(ping); });
}).listen(3000);addEventListener uses the event name from the stream. Reconnection happens without any code of yours.
const source = new EventSource("/events");
source.addEventListener("price", (e) => {
const { value } = JSON.parse(e.data);
document.querySelector("#price").textContent = value;
});
source.onerror = () => {
console.log("Stream dropped; the browser will retry");
};Install with npm install ws. This echoes every message back in upper case to the client that sent it.
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
socket.on("error", console.error);
socket.on("message", (data) => {
socket.send(String(data).toUpperCase());
});
socket.send("connected");
});Unlike EventSource, nothing reconnects for you, so this waits a moment after close and opens a new socket.
function connect() {
const socket = new WebSocket("ws://localhost:8080");
socket.addEventListener("open", () => socket.send("hello"));
socket.addEventListener("message", (e) => console.log(e.data));
socket.addEventListener("close", () => setTimeout(connect, 2000));
return socket;
}
connect();
The server answers a normal GET with Content-Type text/event-stream and keeps the response open. Each message is a few lines such as event:, data: and id:, ended by a blank line. Several data: lines in one message are joined with newlines.
The browser's EventSource parses that stream, fires a message event for unnamed messages and a named event for the rest, and reconnects on its own after a drop. A retry: line sets the wait in milliseconds before it tries again.
The client starts with an HTTP request carrying an Upgrade header, and the server answers 101 Switching Protocols. From then on the connection carries WebSocket frames, not HTTP, and either side can send at any time.
That freedom has a cost. You write your own reconnect logic, your own message format, often JSON with a type field, and your own heartbeat. A proxy that does not pass the Upgrade header along will also stop the connection before it starts.
If the browser mostly listens, choose SSE. Live scores, build logs, order status, notifications and streamed text all fit.
If the browser talks as much as it listens, or the data is binary, choose a WebSocket. Multiplayer games, collaborative editors and chat rooms all fit there.
Not on the stream itself. The event stream only flows from server to client, so the browser sends its messages with ordinary fetch requests. For chat-like traffic in both directions, a WebSocket is the cleaner fit.
Call close() on the client. From the server side, the HTML standard says a response of 204 No Content tells the client to stop reconnecting.
No. The constructor takes only a URL and an options object whose one setting is withCredentials. Use a cookie, a short-lived token in the URL, or a fetch-based reader if you need custom headers.
No. The text/event-stream format is still a good choice for one-way feeds because it runs through normal HTTP servers, proxies and auth with no protocol switch.
Both keep one long-lived connection per client, so the difference is small. What matters more is whether your host allows long-lived responses or protocol upgrades at all, so check its limits on request duration first.
Related: WebRTC vs WebSocket, GraphQL subscriptions, Express.js tutorial, JavaScript async and fetch, CORS explained.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.