If a web app needs live updates, choose the transport based on who needs to send messages and how often. Server-Sent Events (SSE) keep a regular HTTP response open for updates from server to browser. WebSockets keep a connection open for messages in both directions.
For a progress bar, deployment log, or notification feed, start with SSE and send user actions through ordinary HTTP requests. For chat, collaborative editing, or a game where the browser and server exchange frequent messages, consider WebSockets. Neither protocol stores events for you, and both need deliberate authentication, reconnect, and cleanup behavior.
Quick comparison
| Need | Server-Sent Events | WebSockets |
|---|---|---|
| Message direction | Server to browser; the browser can reply with normal HTTP requests | Browser and server can both send messages |
| Browser API | EventSource | WebSocket |
| Connection | Long-lived HTTP response with text/event-stream content | HTTP opening handshake followed by the WebSocket protocol |
| Reconnection | EventSource reconnects automatically; the server can resume from an event ID if it keeps the history | Your application implements reconnect, resubscription, and any recovery |
| Message format | UTF-8 text events with optional event names and IDs | Text or binary messages, with an application-defined message format |
| Flow control | No application-level pull API on EventSource; limit or combine updates | The classic browser WebSocket API has no receive-side backpressure |
| Best starting point | One-way status, alerts, and live feeds | Interactive features with frequent two-way messages |
SSE is not a lesser version of WebSockets. A simple HTTP stream is often easier to operate when the server is the only side pushing updates. WebSockets are useful when the interaction actually needs a two-way channel.
What an SSE stream looks like
An SSE endpoint returns an HTTP response with the text/event-stream media type and leaves it open. Each event is a group of text lines followed by a blank line. For example, a server might stream:
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
retry: 3000
id: 42
event: progress
data: {"completed":8,"total":12}
: keep-alive
The event line gives the message a name, id labels it for reconnection, and retry suggests a reconnection delay in milliseconds. A line beginning with : is a comment rather than a message; servers can send these periodically to keep an idle connection from timing out. The final blank line ends the event. The server must flush events as it writes them, and any reverse proxy in the path must not hold the response until its buffer fills.
In a browser, EventSource opens the stream and dispatches named events:
const source = new EventSource("/events");
source.addEventListener("progress", (event) => {
const progress = JSON.parse(event.data);
console.log(`${progress.completed} of ${progress.total}`);
});
Use a separate fetch() or form request when the browser needs to change something on the server. This keeps the live feed one-way and lets ordinary HTTP handle commands, validation, and errors.
When a connection drops, EventSource attempts to reconnect. If an event has an id, the browser can send its last event ID on the next connection. That is a resume hint, not a message queue: the server must retain or reconstruct events and decide what to replay. If your server does not keep history, the client may need to request a fresh snapshot after reconnecting. Design consumers to tolerate duplicate events.
Call source.close() when the page or component no longer needs the feed. Otherwise, a browser can keep reconnecting in the background.
What changes with WebSockets
A WebSocket connection begins with a browser request to upgrade the connection. Once the server accepts the handshake, either side can send messages without waiting for the other to make a new request. The browser API supports text and binary data, but it does not define your application's message schema.
const socket = new WebSocket("wss://app.example.com/realtime");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "subscribe", room: "project-42" }));
});
socket.addEventListener("message", ({ data }) => {
console.log("Server update:", JSON.parse(data));
});
socket.addEventListener("close", () => {
console.log("Connection closed");
});
This client assumes the server sends JSON text. A production client should also handle errors, decide whether and when to reconnect, restore subscriptions, and recover changes that occurred while disconnected. Use wss:// for a secure connection from an HTTPS page.
The server needs a WebSocket implementation for its runtime. It must authenticate the connection, validate the browser's Origin, authorize subscriptions and incoming messages, and release per-connection resources when the socket closes. The browser constructor does not let application code attach arbitrary request headers, so plan authentication before choosing the transport. Do not treat a successful connection as permission to access every room or action.
Compare the operational details
Authentication and browser security
Browser EventSource and WebSocket constructors do not provide a general way to set an Authorization header. For SSE, a same-origin secure session cookie is often the simplest option. Cross-origin SSE can use withCredentials, but the server must configure CORS for the exact allowed origin and credentials; wildcard origins do not work for credentialed requests.
WebSocket servers should check the handshake's Origin against an allowlist and enforce the user's access rights on every subscription and action. CORS response headers are not a substitute for that check. Avoid putting long-lived bearer tokens in a URL: URLs can be copied into logs and monitoring systems. If your architecture requires a URL ticket, make it short-lived, narrowly scoped, and single-use.
Use HTTPS for the page and the live connection. Treat messages as untrusted input, validate their shape and size on the server, and avoid rendering received text as HTML.
Reconnects and missed messages
SSE saves client code for basic reconnects, but it does not promise durable delivery. If updates represent important changes, assign stable IDs and keep enough history to replay them, or make the client reload a current snapshot. Make replay idempotent so a repeated event does not apply the same change twice.
With WebSockets, implement a retry policy with a delay and jitter rather than reconnecting in a tight loop. After reconnecting, reauthenticate if needed, resubscribe, and fetch state that may have changed while offline. A socket connection by itself is not a reliable event log.
Proxies, timeouts, and connection limits
An SSE response is ordinary HTTP, but infrastructure can still interfere with a long-lived response. Check that your server flushes each event, disable response buffering for the streaming route where needed, and send small comment heartbeats often enough to satisfy the idle timeouts in your own proxy and load balancer. Do not copy a proxy-specific setting without checking which proxy is actually in front of your app.
WebSocket deployments need the proxy to forward the protocol upgrade to the backend and must set suitable idle timeouts. For example, NGINX documents that the Upgrade and Connection headers need explicit handling when proxying WebSockets. Test through the production-like route, not just against a server on localhost.
SSE can also use up browser connections if a page opens many streams. MDN notes a low per-browser, per-origin limit for SSE over HTTP/1.1 (commonly six); with HTTP/2, the client and server negotiate the number of simultaneous streams. Prefer one multiplexed event feed per page or application where possible, and verify the protocol your deployment actually negotiates.
Backpressure and busy feeds
The classic browser WebSocket API does not provide receive-side backpressure. If messages arrive faster than the app can process them, memory use or CPU load can grow. Its bufferedAmount property reports queued outgoing data; it does not slow incoming messages.
EventSource does not expose a pull-based stream reader either. For either transport, set message-size and rate limits, combine repetitive updates, and send a current snapshot instead of every intermediate state when old updates no longer matter. If a client cannot keep up, define how it can resynchronize rather than letting a queue grow without bound.
Choose by interaction, not by novelty
Choose SSE when:
- The server publishes job progress, build output, alerts, or a live dashboard.
- The browser sends occasional commands that fit ordinary
fetch()or form requests. - Text events and straightforward reconnect behavior are a good match.
Choose WebSockets when:
- Both sides send messages frequently, such as chat, shared editing, or multiplayer interactions.
- The application needs one long-lived channel for several message types or binary payloads.
- You are prepared to operate reconnect, authorization, rate limits, and connection cleanup in your own application.
Consider WebTransport when the application needs features that are not just "more real time," such as independent streams, unreliable datagrams, or transport-level backpressure. It uses HTTP/3 and has more deployment considerations than SSE or WebSockets. Its support has improved, but check the browser versions and server infrastructure your users rely on before making it a requirement.
A practical decision checklist
Before choosing, answer these questions:
- Which side initiates most messages? If the server mainly publishes, try SSE. If both sides send continuously, try WebSockets.
- Can the client recover state after a disconnect? Decide whether it needs event replay, a snapshot request, or both.
- How will users authenticate? Confirm how the browser API, cookies, CORS, and your server's origin checks fit together.
- What happens when the client is slow? Limit or coalesce updates and define how it resynchronizes.
- Will your proxy support a long-lived connection? Test buffering, idle timeouts, upgrades, and reconnects in the deployed path.
- How will you test failures? Cover disconnects, duplicate events, expired sessions, denied subscriptions, oversized messages, and a server restart. Our guide to API testing and documentation workflows for small teams has useful patterns for checking these contracts.
For a new feature, implement the simplest flow that meets the interaction: start with SSE for server-pushed updates and ordinary HTTP for user actions. Move to WebSockets when you can point to a real two-way requirement. Choose WebTransport only when its specific stream or datagram behavior justifies the extra compatibility and infrastructure checks.
Sources
- MDN: Using server-sent events
- MDN: CORS
- WHATWG HTML Standard: Server-sent events
- MDN: WebSocket API
- MDN: Writing WebSocket client applications
- The WebSocket Protocol (RFC 6455)
- The OAuth 2.0 Authorization Framework: Bearer Token Usage (RFC 6750)
- NGINX: WebSocket proxying
- NGINX: Proxy module and response buffering
- MDN: WebTransport API