The Future: HTTP/2 & WebSockets
Standard HTTP/1.1 was designed in 1997. The web has changed dramatically since then — we now load pages with hundreds of assets, stream video, build real-time collaborative tools, and expect sub-second responses. HTTP/2 and WebSockets were designed to meet these demands.
1. The Problem with HTTP/1.1
HTTP/1.1 introduced persistent connections (keep-alive), so you didn't need a new TCP connection for every request. But a single connection could only handle one request at a time:
HTTP/1.1 — Sequential (Head-of-Line Blocking):
Time ────────────────────────────────────────────────────────────────►
│
│ Connection 1: [GET styles.css] ... [GET logo.png] ... [GET app.js]
│ Connection 2: [GET fonts.woff] ... [GET icon.svg] ... [GET data.json]
│ Connection 3: [GET hero.jpg] ... [GET footer.jpg]
│
│ Browsers open 6 parallel connections MAX to one server
│ Each connection processes requests one at a time
│ → Large resources block all smaller ones behind them
This is called Head-of-Line (HOL) Blocking — like a queue at a checkout where one slow customer blocks everyone behind them.
2. HTTP/2 — The Multiplexed Highway
HTTP/2 (standardised in 2015) keeps a single TCP connection but allows multiple requests and responses interleaved on the same connection simultaneously.
HTTP/2 — Multiplexed (same TCP connection):
Time ────────────────────────────────────────────────────────────────►
│
│ Single Connection:
│ Stream 1: [GET styles.css] ──────────────────► [Response]
│ Stream 3: [GET logo.png] ─────────────────────────────►
│ Stream 5: [GET app.js] ──────────────────────► [Response]
│ Stream 7: [GET fonts.woff] ────────────────► [Response]
│ Stream 9: [GET data.json] ────────────────────────────►
│
│ All streams in parallel on one connection
│ → No head-of-line blocking (at HTTP level)
Key HTTP/2 Features
| Feature | What it does | Benefit |
|---|---|---|
| Multiplexing | Multiple concurrent streams on one connection | Eliminates HTTP-level HOL blocking |
| Header Compression (HPACK) | Compresses and deduplicates headers across requests | Headers that repeat (like auth tokens) are sent only once |
| Binary Protocol | Uses binary frames instead of text | Faster to parse, less error-prone |
| Stream Prioritisation | Client hints which resources are most important | Critical CSS loads before non-critical images |
| Server Push | Server proactively sends resources the client will need | Browser gets CSS/JS before it even parses the HTML |
HTTP/2 vs HTTP/1.1 Performance
| Scenario | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Page with 100 resources | 6 parallel connections, sequential per conn | 1 connection, all 100 in parallel |
| Repeated headers (identical auth) | Sent in full on every request | Only sent once (HPACK deduplication) |
| CSS/JS needed immediately | Browser discovers after parsing HTML | Server can push before browser asks |
Server Push Example (Node.js)
// Using the 'http2' built-in Node.js module
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
});
server.on('stream', (stream, headers) => {
if (headers[':path'] === '/') {
// Push CSS before the client asks for it
stream.pushStream({ ':path': '/styles.css' }, (err, pushStream) => {
pushStream.respondWithFile('./public/styles.css', {
'content-type': 'text/css'
});
});
// Serve the HTML page
stream.respondWithFile('./public/index.html', {
'content-type': 'text/html'
});
}
});
server.listen(443);
Note: Server Push has been largely deprecated in practice due to implementation complexity and the arrival of HTTP/3. The
<link rel="preload">HTML hint achieves similar results more simply.
3. HTTP/3 — The UDP Revolution
HTTP/2's multiplexing works at the HTTP level, but still runs over TCP. TCP itself has packet-level HOL blocking — if one TCP packet is lost, all HTTP/2 streams on that connection stall waiting for retransmission.
HTTP/3 solves this by running over QUIC instead of TCP.
HTTP/1.1 Stack: HTTP/2 Stack: HTTP/3 Stack:
HTTP HTTP/2 HTTP/3
│ │ │
TCP TCP QUIC (UDP-based)
│ │ │
IP IP IP
│ │ │
Physical Physical Physical
QUIC Features
| Feature | Benefit |
|---|---|
| Built-in TLS 1.3 | Encrypted + connection established in 0-RTT on resumption |
| Per-stream loss recovery | Dropped packet only affects its own stream — others continue |
| Connection migration | Switch from Wi-Fi to 4G without reconnecting (same connection ID) |
| Reduced handshake | Combines TCP + TLS handshake into one round-trip |
HTTP/2 over TCP — packet loss:
Stream 1: [packet 1] [packet 2] [LOST packet 3] ... waiting ... [packet 4]
Stream 3: ↑ BLOCKED waiting for retransmit!
Stream 5: ↑ BLOCKED too!
HTTP/3 over QUIC — packet loss:
Stream 1: [chunk 1] [chunk 2] [LOST chunk 3] ... waiting ... [chunk 4]
Stream 3: → continues independently ✅
Stream 5: → continues independently ✅
4. WebSockets — Real-Time Bidirectional Communication
Standard HTTP always follows the pattern: client asks → server answers. The server can never send data to a client that the client didn't ask for (known as "polling" workarounds).
WebSockets open a persistent, full-duplex (bidirectional) channel where:
- The client can send to the server at any time
- The server can send to the client at any time
- No request-response pairing needed
WebSocket Handshake
WebSockets start as an HTTP connection, then upgrade:
Client → Server:
GET /chat HTTP/1.1
Host: chat.nandhoo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
[HTTP connection is now a WebSocket connection]
[Data flows freely in both directions]
After the 101 Switching Protocols response, the connection is no longer HTTP — it's WebSocket frames until either side closes it.
WebSocket API in JavaScript
// Connect to a WebSocket server
const socket = new WebSocket('wss://chat.nandhoo.com/rooms/42');
// ^^^
// wss:// = WebSocket Secure (TLS)
// ws:// = WebSocket (plain, never use in production)
// Connection opened
socket.addEventListener('open', () => {
console.log('WebSocket connected!');
// Send a message to the server
socket.send(JSON.stringify({
type: 'join',
room: 42,
user: 'Alice',
message: 'Hello everyone!'
}));
});
// Receive messages from the server
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log(`${data.user}: ${data.message}`);
appendMessageToChat(data);
});
// Handle errors
socket.addEventListener('error', (err) => {
console.error('WebSocket error:', err);
});
// Connection closed
socket.addEventListener('close', (event) => {
console.log(`Disconnected. Code: ${event.code}, Reason: ${event.reason}`);
if (event.code !== 1000) {
// Reconnect after 3 seconds (exponential backoff in production)
setTimeout(() => reconnect(), 3000);
}
});
// Close the connection
function leaveChat() {
socket.close(1000, 'User left the room'); // 1000 = normal closure
}
WebSocket Close Codes
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Endpoint going away (browser navigating away) |
| 1006 | Abnormal closure (connection dropped, no close frame) |
| 1008 | Policy violation |
| 1011 | Server encountered an unexpected error |
5. Server-Sent Events (SSE) — One-Way Real-Time
SSE is a simpler alternative to WebSockets when you only need the server to push data to the client (not the other way).
// Client: subscribe to a stream of events
const eventSource = new EventSource('https://api.nandhoo.com/events/live-scores');
eventSource.addEventListener('score', (event) => {
const score = JSON.parse(event.data);
updateScoreboard(score);
});
eventSource.addEventListener('error', () => {
console.log('Connection lost, browser will reconnect...');
});
// Stop listening
function stopWatching() {
eventSource.close();
}
Server sends (plain HTTP response with text/event-stream content type):
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
event: score
data: {"team":"A","points":42}
event: score
data: {"team":"B","points":38}
SSE vs WebSockets
| SSE | WebSockets | |
|---|---|---|
| Direction | Server → Client only | Bidirectional |
| Protocol | HTTP | WebSocket (after upgrade) |
| Auto-reconnect | ✅ Built-in | ❌ Must implement manually |
| Text only | ✅ Text/UTF-8 | Text and binary |
| CORS support | ✅ Standard | ✅ With origin checks |
| Proxy/firewall friendly | ✅ (plain HTTP) | ⚠️ Some proxies block WS |
| Best for | Live feeds, notifications, progress | Chat, games, collaboration |
6. Long Polling — The HTTP/1.1 Workaround
Before WebSockets were widely supported, developers used long polling:
// Long Polling approach (legacy — avoid if WebSockets are available)
async function poll() {
try {
// Server holds the request open for up to 30 seconds
// If new data arrives, it responds immediately
// If timeout, it returns empty and client re-polls
const res = await fetch('/api/updates?last=1711864280');
const data = await res.json();
if (data.updates.length > 0) processUpdates(data.updates);
} finally {
// Immediately poll again
setTimeout(poll, 100);
}
}
poll();
Problems with long polling:
- Wastes server resources keeping connections open
- Still has the request-response overhead
- Hard to handle reconnections and errors
Use WebSockets or SSE instead for real-time applications.
7. Choosing the Right Technology
Do you need real-time updates?
│
├── No → Use regular HTTP fetch/REST APIs
│
└── Yes → Do you need bidirectional communication?
│
├── Yes → WebSockets
│ Examples: chat, multiplayer games, collaborative editors
│ live coding environments, trading dashboards
│
└── No → Server-Sent Events (SSE)
Examples: live notifications, progress tracking, live scores
news feeds, stock tickers, deployment logs
8. A Full Chat Application Example
class ChatRoom {
constructor(roomId, username) {
this.roomId = roomId;
this.username = username;
this.socket = null;
this.retries = 0;
}
connect() {
this.socket = new WebSocket(
`wss://chat.nandhoo.com/rooms/${this.roomId}`
);
this.socket.onopen = () => {
this.retries = 0;
this.send({ type: 'join', username: this.username });
};
this.socket.onmessage = ({ data }) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'message': this.displayMessage(msg); break;
case 'presence': this.updatePresence(msg); break;
case 'typing': this.showTypingIndicator(msg); break;
}
};
this.socket.onclose = (event) => {
if (event.code !== 1000 && this.retries < 5) {
const delay = Math.min(1000 * 2 ** this.retries, 30000); // Exponential backoff
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => { this.retries++; this.connect(); }, delay);
}
};
this.socket.onerror = (err) => console.error('WS error', err);
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
sendMessage(text) {
this.send({ type: 'message', text, username: this.username });
}
disconnect() {
this.retries = 5; // Prevent reconnection
this.socket?.close(1000, 'Left the room');
}
displayMessage({ username, text, timestamp }) {
const div = document.createElement('div');
div.className = username === this.username ? 'mine' : 'theirs';
div.innerHTML = `<strong>${username}</strong>: ${text}`;
document.querySelector('#messages').appendChild(div);
}
updatePresence({ users }) {
document.querySelector('#users').textContent = users.join(', ');
}
showTypingIndicator({ username }) {
document.querySelector('#typing').textContent = `${username} is typing...`;
setTimeout(() => document.querySelector('#typing').textContent = '', 3000);
}
}
const room = new ChatRoom(42, 'Alice');
room.connect();
9. HTTP Version Comparison
| Feature | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Year | 1997 | 2015 | 2022 |
| Transport | TCP | TCP | QUIC (UDP) |
| Multiplexing | No (HOL blocking) | Yes (streams) | Yes (per-stream) |
| Header format | Text | Binary + HPACK compression | Binary + QPACK compression |
| TLS required | Optional | Effectively required | Always (built-in) |
| Server Push | No | Yes (deprecated) | No |
| Connection migration | No | No | Yes (mobile!) |
| Adoption (2024) | ~20% | ~35% | ~30% |
10. Mini Exercises
- Open DevTools → Network tab → Check the "Protocol" column. What HTTP version does Google.com use? Your bank?
- In the console, open a WebSocket to the free echo server:
new WebSocket('wss://echo.websocket.events'). Listen for messages and send one with.send('Hello from Nandhoo!'). - Subscribe to an SSE stream:
new EventSource('https://sse.dev/test'). What events arrive? - Read about HTTP/3 on cloudflare.com. Which feature do you think is most impactful for mobile users and why?
- Build a simple typing indicator: use a WebSocket to broadcast "typing" events when a user types, and show it on the screen.
11. Key Terms
| Term | Meaning |
|---|---|
| HOL Blocking | Head-of-Line Blocking — one slow request blocks all requests behind it |
| Multiplexing | Multiple concurrent HTTP streams over a single connection |
| HPACK | Header compression algorithm used in HTTP/2 |
| QUIC | UDP-based transport protocol used by HTTP/3 |
| Server Push | Server proactively sending resources before the client asks |
| WebSocket | Full-duplex, persistent connection — both sides can send anytime |
| SSE | Server-Sent Events — one-way real-time stream from server to client |
| 101 Switching Protocols | HTTP status upgrading the connection to WebSocket |
| wss:// | WebSocket Secure — WebSocket over TLS |
| Long Polling | Legacy real-time technique — client keeps re-requesting until data arrives |
| Exponential Backoff | Reconnection strategy — double the wait time after each failure |
Congratulations! You've completed the HTTP & Networking module. You now understand the full journey of data from client to server and back, encrypted and secure.
Back to Introduction: HTTP & Networking Overview