Real-Time Web: An In-Depth Comparison of SSE and WebSocket
Choosing between SSE and WebSocket for server push? This article explains their fundamental differences, ideal use cases, proxy and firewall compatibility, connection limits, and provides clear guidance on when to use each.
Key Points
- Topic Focus: Next.js、TypeScript
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Anyone who has built real-time features has faced this choice: when the server needs to push data to the browser, should you use SSE (Server-Sent Events) or WebSocket?
Most articles online say that both work and that it depends on your requirements. That is correct but unhelpful. When you actually need to decide, you want to know: how do they differ at the protocol layer? Which one works more reliably through proxies? Why does nearly every LLM use SSE for streaming output? When is WebSocket essential? This article answers all of these questions.
1. The Essential Difference: Fundamentally Different Protocols
Set the APIs aside and look at the protocols first. This is the source of every other difference.
SSE is not a new protocol; it is simply HTTP. The client sends an ordinary HTTP GET request. The server keeps the connection open and continuously writes data to the response body in the simple format data: xxx\n\n. The browser receives it through the native EventSource API.
GET /events HTTP/1.1
Accept: text/event-stream
---
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"price": 4200}
data: {"price": 4201}
WebSocket is an independent protocol. It uses HTTP for an initial upgrade handshake (101 Switching Protocols). After the handshake succeeds, that TCP connection is taken over: it stops speaking HTTP and switches to WebSocket's own binary framing protocol. It is also full-duplex, so the client and server can send messages to each other at any time, simultaneously.
GET /ws HTTP/1.1
Upgrade: websocket
---
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
(From this point, the TCP connection leaves HTTP and enters bidirectional frame communication)
In one sentence: SSE is an HTTP long-lived connection for one-way server-to-browser pushes; WebSocket is an independent protocol for bidirectional real-time communication. Directionality, protocol, and complexity all follow from this distinction.
2. SSE's Advantages: Simple and HTTP-Friendly
Nearly all of SSE's advantages come from the fact that it is simply HTTP.
First, it passes through firewalls, reverse proxies, and CDNs with almost no extra work. Because it uses ordinary HTTP requests and responses, nginx, Cloudflare, and corporate network proxies can usually handle it correctly by default. You do not need to configure protocol upgrades or worry about a middlebox that does not recognize the WebSocket handshake.
Second, browsers reconnect automatically. If an EventSource connection drops, the browser automatically attempts to reconnect and sends a Last-Event-ID header, allowing the server to resume from the interruption. This behavior is built into the protocol, so you do not need to implement reconnection yourself.
// Client: three lines are enough
const es = new EventSource('/api/events');
es.onmessage = (e) => console.log(JSON.parse(e.data));
es.onerror = () => { /* The browser reconnects automatically; no manual handling needed */ };
Third, the API is minimal. The server only needs to set the correct response headers and write text to the stream. On the client, EventSource receives messages through onmessage or addEventListener. The mental overhead is extremely low.
Fourth, HTTP/2 provides native multiplexing. Multiple SSE streams can share one HTTP/2 connection, avoiding the connection limit imposed by HTTP/1.1, which is covered in the next section.
3. SSE's Disadvantages: One-Way, Text-Based, and Connection-Limited
Its disadvantages also follow from its one-way HTTP nature.
First, it is one-way. The server can push data to the client, but the client must use ordinary HTTP, such as a POST request, to send data to the server. For chat or collaborative editing, where both sides exchange messages, SSE becomes awkward: you need separate paths for HTTP upstream and SSE downstream, and you must synchronize their state yourself.
Second, it is primarily text-based. SSE transports UTF-8 text. Binary data must be Base64-encoded, which adds size overhead. It is unsuitable for transmitting images or audio and video streams.
Third, browsers limit same-origin connections under HTTP/1.1. Browsers cap the number of HTTP/1.1 connections to the same domain, typically at six in Chrome and Firefox. Each SSE stream occupies one connection, so six SSE streams can exhaust the HTTP/1.1 connection pool and even block ordinary requests. This limitation disappears under HTTP/2, so production environments should always use HTTP/2.
In addition, EventSource only supports GET and does not allow custom request headers, such as a Bearer token. Authentication usually has to rely on cookies or URL parameters, which can be slightly awkward.
4. WebSocket's Advantages: Bidirectional, Binary-Friendly, and Low-Latency
WebSocket's strengths directly address SSE's weaknesses.
First, it provides full-duplex communication. This is the central reason it exists. Clients and servers can send messages to each other at any time, making it a natural fit for chat, collaboration, multiplayer games, real-time whiteboards, and other interactive scenarios.
Second, it handles binary data well. WebSocket frames can carry ArrayBuffer or Blob data, avoiding encoding overhead for game synchronization, audio and video transmission, and large volumes of structured binary data.
Third, it has low protocol overhead and low latency. After the handshake, each data frame has a header of only 2-14 bytes, far less than the bulky headers attached to every HTTP request. WebSocket is significantly more efficient for frequent small messages, such as position updates.
// Client
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send(JSON.stringify({ type: 'chat', text: 'hi' })); // Bidirectional!
5. WebSocket's Disadvantages: Complex Proxying and Manual State Management
First, its independent protocol complicates proxies and CDNs. Although the handshake uses HTTP, the connection fails if any intermediary, such as a corporate proxy, CDN, or load balancer, does not support Upgrade. nginx requires a separate proxy_set_header Upgrade $http_upgrade; configuration, Cloudflare has numerous WebSocket-specific considerations, and HTTP/2 proxies also involve the :protocol extension (CONNECT/extended negotiation). SSE requires almost no attention in this area.
Second, the infrastructure cost is higher. WebSocket uses stateful long-lived connections, so the server must maintain state for every connection, including users, subscriptions, and heartbeats. Load balancers therefore cannot simply use round-robin distribution, and horizontal scaling requires a message bus such as Redis Pub/Sub to broadcast across nodes. Deployment complexity rises substantially.
Third, you must build every reliability feature yourself. The browser's native WebSocket API does not reconnect automatically, so you must implement reconnection and backoff. The protocol does not provide application-level heartbeats, so you must periodically use ping/pong to detect dead connections and prevent intermediary network devices from silently terminating them. Authentication is also entirely custom, commonly using a token during the handshake or placing it in a subprotocol. SSE provides most of these capabilities out of the box.
6. Choosing Between Them: Which Scenario Needs Which Option?
The differences can be condensed into one table:
| Dimension | SSE | WebSocket |
|---|---|---|
| Communication direction | Server → client (one-way) | Full-duplex, bidirectional |
| Underlying protocol | HTTP | Independent protocol (uses HTTP for the handshake) |
| Data format | Text (UTF-8) | Text + binary |
| Browser API | EventSource (native, automatic reconnection) | WebSocket (manual reconnection required) |
| Proxy/CDN/firewall compatibility | Friendly (it is HTTP) | Requires additional configuration |
| Connection limits | About six per origin under HTTP/1.1 (no such limit under HTTP/2) | No hard browser-level limit |
| Heartbeats/reconnection/authentication | Built into the protocol or simple | Must all be implemented manually |
| Server complexity | Low (write an HTTP response stream) | High (stateful long-lived connections) |
| Horizontal scaling | Relatively simple | Requires a message bus |
| Typical scenarios | Notifications, market data, log streams, AI streaming | Chat, collaboration, multiplayer games |
The decision comes down to three rules:
- One-way push → SSE. For in-app notifications, stock quotes, build log streams, and AI token streaming, where the server only sends and the client only receives, choose SSE. It is simple, reliable, and adds virtually no infrastructure burden.
- Bidirectional real-time communication → WebSocket. For instant messaging, collaborative editing, multiplayer games, and real-time synchronization, where the client also sends frequent messages to the server, choose WebSocket. Do not force two SSE/HTTP paths to imitate it; synchronizing their state will make you question your life choices.
- When uncertain, start with SSE. SSE costs a fraction of WebSocket to implement. Get it working with SSE first, then upgrade to WebSocket only if you truly hit the ceiling of frequent client-to-server updates. Most real-time requirements never reach that point.
7. Lessons for AI Applications: Why LLM Streaming Uses SSE
The current wave of AI applications has almost redefined SSE's relevance. Open the streaming APIs from OpenAI, Anthropic, Google, or the Vercel AI SDK, and you will find SSE almost everywhere under the hood. That is no coincidence.
SSE's defining characteristics align perfectly with LLM streaming output:
- LLM streaming is one-way push. The model generates one token and pushes one token to the frontend. The frontend does not need to send messages back during generation. One-way communication is SSE's ideal use case.
- HTTP-based means free infrastructure reuse. LLM services usually run behind standard HTTP gateways. SSE lets them directly reuse existing CDNs, authentication, rate limiting, and observability systems without building a separate WebSocket stack.
- It is simple and reliable. Automatic reconnection, the easily debugged
text/event-streamformat, and native browser support all help. In a system that already contains uncertainty, such as model timeouts, rate limits, and content moderation, the transport layer should be as simple as possible. - The ecosystem is standardized around it. OpenAI's
stream: true, the Vercel AI SDK'suseChat, and various LangChain streaming wrappers use SSE by default. Choosing SSE means aligning with the mainstream ecosystem.
A typical Next.js streaming response is simply a Route Handler that emits SSE from the server:
// app/api/chat/route.ts (illustrative)
export async function POST(req: Request) {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for await (const token of llmStream(await req.json())) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ token })}\n\n`));
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
});
}
Once the frontend receives the stream, it reads and renders data incrementally to create a typewriter effect. The entire path contains no WebSocket connection. This is why SSE has found a second life in the AI era.
Summary
- The essential difference is the protocol: SSE is a one-way HTTP long-lived connection, while WebSocket is an independent full-duplex protocol. Every advantage and disadvantage follows from this distinction.
- SSE wins on simplicity and HTTP compatibility: It passes through proxies and CDNs without extra configuration, reconnects automatically, offers a minimal API, and has the lowest infrastructure cost.
- WebSocket wins on bidirectional communication and performance: Full-duplex communication, binary support, and low frame overhead make it essential for chat, collaboration, and games.
- WebSocket carries real costs: Its independent protocol requires proxy configuration, stateful connections complicate horizontal scaling, and reconnection, heartbeats, and authentication must all be implemented manually.
- AI streaming brought SSE back: LLM streaming is fundamentally one-way push, and ecosystem standardization has made SSE the de facto mainstream choice.
The key rule: if the requirement is one-way, SSE is enough. Only bidirectional communication requires WebSocket, and 90% of supposedly real-time requirements are actually one-way.
One practical reminder: always pair SSE with HTTP/2 to avoid HTTP/1.1's six-connection ceiling. For WebSocket, configure heartbeats and reconnection properly so connections do not silently die somewhere in the network. Get these two details right, and your technology choice will remain reliable.
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
How Should a Technical Community Design Its Tag System and Recommendation Feed?
Centered on independent development, Next.js technology trends, engineering practices, and the indie developer perspective.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.