HTTP/2 Server Push vs WebSocket #
You searched for “HTTP/2 Server Push vs WebSocket” expecting two ways to stream live data from server to browser. They are not comparable: HTTP/2 Server Push was a cache-priming mechanism for static assets, never a server-to-client messaging channel, and it has been removed from Chrome since version 106. If you need to deliver application events to running JavaScript, the real choice is between Server-Sent Events (over HTTP/2 multiplexing) and WebSocket. This guide untangles the confusion and gives you a concrete decision.
Root cause: Server Push pushes bytes, not events #
HTTP/2 Server Push let a server send a PUSH_PROMISE frame alongside a response for index.html, proactively shipping app.css and app.js into the browser’s HTTP cache before the parser requested them. The goal was latency: skip one round trip on page load. Crucially, pushed responses landed in the cache and were consumed by the browser’s resource loader. There is no JavaScript API to observe a pushed resource as a stream of messages — no event fires, no callback runs, your code cannot read frames as they arrive.
That design also failed in practice. Servers over-pushed assets the client already had cached, wasting bandwidth, and the PUSH_PROMISE frame raced the client’s cache lookup. Chrome shipped the removal in version 106 (2022), and the broader ecosystem treats 103 Early Hints as the replacement for the load-latency use case. So Server Push is gone for both of its possible interpretations: it never delivered application events, and it no longer primes the cache either.
Server-initiated data — a price tick, a chat message, a job-status change — needs a transport that surfaces in JavaScript. Two qualify, and both are covered in the parent WebSocket vs SSE vs WebRTC: Protocol Guide:
- Server-Sent Events (SSE): a long-lived HTTP response with
Content-Type: text/event-stream, consumed by the browser’sEventSource. Unidirectional (server → client), text only, auto-reconnecting. - WebSocket: a protocol upgrade to full-duplex framed messaging over one TCP connection. Bidirectional, binary or text, no built-in reconnect.
How HTTP/2 changes the SSE math #
The historic knock against SSE was HTTP/1.1’s six-connections-per-origin limit: every EventSource consumed one of those slots, so a few streams plus normal page traffic exhausted the browser’s connection budget. HTTP/2 dissolves that constraint. All requests to an origin share a single TCP connection and are multiplexed as independent streams, so a dozen SSE streams plus your REST calls coexist on one connection with no head-of-line blocking at the HTTP layer.
That is the genuinely useful “HTTP/2 + push” story people half-remember: not Server Push, but SSE riding HTTP/2 multiplexing. You get server-initiated events delivered to onmessage, automatic reconnection with Last-Event-ID resumption, transparent traversal of proxies and CDNs (it is just an HTTP response), and no connection-count penalty. For server-to-client-only workloads this is often the lowest-friction option — see when to use WebSockets over Server-Sent Events for the full trade-off matrix.
WebSocket wins when the client must talk back on the same channel with low latency: chat input, collaborative-editing operations, game input, or any acknowledgment-heavy mutation protocol. SSE can pair with fetch/POST for the upstream direction, but that splits your protocol across two transports and loses ordering guarantees between them. If upstream traffic is frequent and latency-sensitive, full-duplex framing is the right primitive.
| Dimension | HTTP/2 Server Push | SSE over HTTP/2 | WebSocket |
|---|---|---|---|
| Delivers events to JS | No (cache only) | Yes (onmessage) |
Yes (onmessage) |
| Direction | Server → cache | Server → client | Full-duplex |
| Status | Removed (Chrome 106+) | Standard, supported | Standard, supported |
| Payload | Static assets | UTF-8 text | Text or binary |
| Auto-reconnect | n/a | Built into EventSource |
Manual (client logic) |
| Connection cost | Shares page connection | Multiplexed stream | Dedicated TCP socket |
| Best for | (deprecated) | Feeds, notifications, logs | Chat, mutations, gaming |
Resolution: choosing SSE-over-HTTP/2 vs WebSocket #
Pick the transport by direction of traffic. If the browser only receives, serve SSE over HTTP/2. The server side is a plain streaming HTTP response.
import http2 from 'node:http2';
import { readFileSync } from 'node:fs';
const RETRY_MS = 3000; // client reconnect hint, sent once per stream
const TICK_INTERVAL_MS = 1000; // demo push cadence
const server = http2.createSecureServer({
key: readFileSync('key.pem'),
cert: readFileSync('cert.pem'), // HTTP/2 requires TLS in browsers (h2, not h2c)
});
server.on('stream', (stream, headers) => {
if (headers[':path'] !== '/events') {
stream.respond({ ':status': 404 });
return stream.end();
}
stream.respond({
':status': 200,
'content-type': 'text/event-stream', // the EventSource contract
'cache-control': 'no-cache',
// No 'connection: keep-alive' header — it is forbidden in HTTP/2.
});
stream.write(`retry: ${RETRY_MS}\n\n`); // tell EventSource how long to wait before reconnecting
let id = Number(headers['last-event-id'] ?? 0); // resume from where the client left off
const timer = setInterval(() => {
id += 1;
// id: lets the browser send Last-Event-ID on reconnect; event/data are the payload.
stream.write(`id: ${id}\nevent: tick\ndata: ${JSON.stringify({ id, ts: Date.now() })}\n\n`);
}, TICK_INTERVAL_MS);
stream.on('close', () => clearInterval(timer)); // stop work when the client disconnects
});
server.listen(8443);
The browser consumes it with native EventSource — no library, reconnection handled for you:
const es = new EventSource('/events'); // multiplexes onto the existing HTTP/2 connection
es.addEventListener('tick', (e) => {
const { id, ts } = JSON.parse((e as MessageEvent).data);
console.log('tick', id, ts);
});
es.onerror = () => {
// EventSource auto-reconnects using the `retry:` hint and Last-Event-ID.
console.warn('SSE dropped; browser will retry');
};
If the client must send frequent, low-latency messages back, switch to WebSocket and accept the dedicated socket. The upgrade lives on HTTP/1.1; native WebSocket-over-HTTP/2 (RFC 8441 :protocol) exists but has thin server support, so most deployments still run ws on a 1.1 endpoint.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString()); // client → server: the direction SSE can't do natively
ws.send(JSON.stringify({ ack: msg.seq })); // server → client: immediate acknowledgment on the same socket
});
});
Rule of thumb: server-to-client only → SSE over HTTP/2; bidirectional and chatty → WebSocket. Reach for Server Push for neither.
Operational checklist #
- Confirm you are not relying on HTTP/2 Server Push for any data delivery — grep your codebase and CDN config for
http2_push/Link: rel=preload; nopush - Disable response buffering on the SSE path at every proxy hop (
proxy_buffering off;in nginx,X-Accel-Buffering: no - Set a
retry:value and emit incrementingid:fields soEventSourcereconnection resumes viaLast-Event-ID - Verify your CDN does not strip
text/event-stream
FAQ #
Can I use HTTP/2 Server Push to send real-time data to JavaScript? #
No. Server Push delivered resources into the browser’s HTTP cache for the resource loader; there is no JavaScript API to read pushed bytes as a message stream. It is also removed from Chrome since version 106. Use SSE or WebSocket for events that reach your code.
Is SSE over HTTP/2 as efficient as WebSocket? #
For server-to-client-only traffic, often more efficient operationally: SSE multiplexes onto the shared HTTP/2 connection, reconnects automatically, and passes through proxies and CDNs as ordinary HTTP. WebSocket opens a dedicated TCP socket and needs custom reconnect logic, but it is the right choice when the client sends frequent, latency-sensitive messages upstream.
What replaced HTTP/2 Server Push for load performance? #
103 Early Hints — the server sends an interim 103 response advertising Link: rel=preload resources so the browser can start fetching them before the final response. It addresses Server Push’s original asset-preloading goal, not real-time messaging.
Does WebSocket run over HTTP/2? #
Standard WebSocket upgrades over HTTP/1.1. RFC 8441 defines bootstrapping WebSocket over an HTTP/2 stream via the :protocol pseudo-header, but server and proxy support is limited, so production deployments typically keep the WebSocket endpoint on HTTP/1.1.
Why did browsers remove HTTP/2 Server Push? #
It rarely improved performance in practice — servers over-pushed assets the client had already cached, and pushed responses raced the client’s cache lookup. The wins were marginal and the bandwidth waste real, so Chrome removed it in version 106 and steered developers to Early Hints.
Related #
- WebSocket vs SSE vs WebRTC: Protocol Guide — the full comparison of every server-push transport for the browser.
- When to Use WebSockets Over Server-Sent Events — the bidirectionality and latency thresholds that tip the decision.
- Real-Time Protocol Selection & Architecture — choosing and operating the right real-time transport end to end.