HTTP/2, TLS, and WebSockets: Modern Application Protocols
How HTTP/2 multiplexes streams over a single connection, how TLS secures communication with encryption and authentication, and how WebSockets enable real-time bidirectional messaging.
Terminology
- HTTP (HyperText Transfer Protocol): an application-layer protocol for transferring hypermedia documents; the foundation of data communication on the web, using a request-response model
- HTTP/1.1: the widely deployed version of HTTP that introduced persistent connections and chunked transfer encoding, but suffers from head-of-line blocking
- HTTP/2: a major revision of HTTP that introduces binary framing, multiplexed streams, header compression, and server push over a single TCP connection
- Stream: in HTTP/2, an independent, bidirectional sequence of frames exchanged between client and server within a single connection; each request-response pair uses its own stream
- Frame: the smallest unit of communication in HTTP/2; each frame has a type (HEADERS, DATA, SETTINGS, etc.) and belongs to a specific stream
- Multiplexing: the ability to send multiple requests and responses simultaneously over a single TCP connection, interleaving frames from different streams
- Head-of-line (HOL) blocking: a condition where a slow or blocked request prevents subsequent requests from being processed; in HTTP/1.1, this occurs because requests on a connection are processed sequentially
- HPACK: the header compression algorithm used by HTTP/2 that reduces overhead by encoding headers as indices into a shared dynamic table
- TLS (Transport Layer Security): a cryptographic protocol that provides privacy and data integrity between two communicating applications through encryption, authentication, and message integrity checks
- Handshake: the initial negotiation between client and server to establish a secure TLS connection, including cipher suite selection, certificate exchange, and key agreement
- Certificate: a digital document issued by a Certificate Authority (CA) that binds a public key to a domain name, allowing clients to verify the server's identity
- Cipher suite: a combination of algorithms used for key exchange, encryption, and message authentication in a TLS connection
What & Why
HTTP/1.1 served the web well for over 15 years, but its design has fundamental limitations. Each TCP connection processes requests sequentially: the second request cannot be sent until the first response is complete (head-of-line blocking). Browsers work around this by opening 6-8 parallel TCP connections per domain, but each connection requires its own TCP handshake and TLS negotiation, wasting time and resources.
HTTP/2 solves this by multiplexing multiple request-response pairs as independent streams over a single TCP connection. Frames from different streams are interleaved, so a slow response on one stream does not block others. Header compression (HPACK) reduces the repetitive overhead of sending similar headers on every request. The result is faster page loads, especially on high-latency connections.
TLS secures the communication channel. Without TLS, anyone on the network path can read and modify HTTP traffic. TLS provides three guarantees: confidentiality (encryption prevents eavesdropping), integrity (message authentication codes detect tampering), and authentication (certificates verify the server's identity). TLS 1.3 streamlined the handshake to just one round trip, making HTTPS nearly as fast as plain HTTP.
WebSockets fill a gap that HTTP's request-response model cannot: real-time, bidirectional communication. With HTTP, the server can only send data in response to a client request. WebSockets upgrade an HTTP connection to a persistent, full-duplex channel where either side can send messages at any time. This is essential for chat applications, live dashboards, collaborative editing, and multiplayer games.
How It Works
HTTP/2 Binary Framing
HTTP/2 replaces HTTP/1.1's text-based protocol with a binary framing layer. All communication is split into frames, each tagged with a stream ID. The connection carries multiple concurrent streams.
Key HTTP/2 features:
Multiplexing: Multiple requests and responses share a single TCP connection. Each stream is independent, so a slow response on stream 1 does not block streams 3 and 5. This eliminates the need for multiple TCP connections.
Header compression (HPACK): HTTP headers are repetitive (same Host, User-Agent, Accept on every request). HPACK maintains a dynamic table of previously sent headers and encodes repeated headers as small indices. First request might send 800 bytes of headers; subsequent requests on the same connection might send 20 bytes.
Stream prioritization: Clients can assign priorities and dependencies to streams, telling the server which resources are most important. The server uses this to allocate bandwidth to critical resources (CSS, JavaScript) before less important ones (images).
Server push: The server can proactively send resources it knows the client will need. When the client requests an HTML page, the server can push the associated CSS and JavaScript files without waiting for the client to parse the HTML and request them.
TLS 1.3 Handshake
TLS 1.3 reduced the handshake from two round trips (TLS 1.2) to one:
- Client Hello: The client sends supported cipher suites and key shares (ephemeral public keys for key exchange) in a single message
- Server Hello + Encrypted Extensions + Certificate + Finished: The server selects a cipher suite, sends its key share, certificate, and finishes the handshake, all in one flight
- Client Finished: The client verifies the certificate, computes the shared secret, and sends its Finished message
After step 2, the server can already send encrypted application data. After step 3, the client can send encrypted data. Total: 1 RTT for the handshake (compared to 2 RTTs in TLS 1.2).
TLS 1.3 also supports 0-RTT resumption: if the client has connected to the server before, it can send encrypted application data in the very first message, achieving zero additional latency for the TLS handshake. The trade-off is that 0-RTT data is vulnerable to replay attacks, so it should only be used for idempotent requests.
Key exchange: TLS 1.3 exclusively uses ephemeral Diffie-Hellman (ECDHE), providing perfect forward secrecy. Even if the server's long-term private key is compromised, past sessions cannot be decrypted because each session used a unique ephemeral key.
WebSocket Protocol
WebSockets start as an HTTP request with an Upgrade header:
- Client sends an HTTP/1.1 request with
Upgrade: websocketand a random key - Server responds with
101 Switching Protocolsand a derived accept key - The TCP connection is now a WebSocket connection: both sides can send frames at any time
WebSocket frames are lightweight: a 2-byte header for small messages (up to 125 bytes payload), 4 bytes for medium messages (up to 65535 bytes), and 10 bytes for large messages. Client-to-server frames are masked with a 4-byte key to prevent cache poisoning attacks on intermediary proxies.
Unlike HTTP's request-response model, WebSocket messages are independent. The server can push data to the client at any time without the client asking. The client can send messages at any time without waiting for a response. Either side can close the connection with a close frame.
Complexity Analysis
| Metric | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Connections per domain | $6$-$8$ (browser limit) | $1$ |
| HOL blocking | Yes (per connection) | No (at HTTP level) |
| Header overhead (repeated) | $\sim 800$ bytes/request | $\sim 20$ bytes (HPACK) |
| Handshake cost (with TLS) | $1 + 2 = 3$ RTTs (TCP + TLS 1.2) | $1 + 1 = 2$ RTTs (TCP + TLS 1.3) |
Time to first byte for a new HTTPS connection:
With TLS 1.3:
With TLS 1.3 0-RTT resumption, the TLS and HTTP request can overlap:
HPACK compression ratio for repeated headers:
WebSocket frame overhead per message:
Client-to-server frames add 4 bytes for the masking key. Compare this to HTTP/1.1 where each request carries hundreds of bytes of headers.
Implementation
ALGORITHM HTTP2Multiplexer(connection, pendingRequests)
INPUT: connection: single TCP connection, pendingRequests: list of HTTP requests
OUTPUT: responses for all requests
BEGIN
nextStreamID <- 1 // client-initiated streams use odd IDs
activeStreams <- empty map
responses <- empty map
// Send all requests as HEADERS frames
FOR EACH request IN pendingRequests DO
streamID <- nextStreamID
nextStreamID <- nextStreamID + 2
headersFrame <- CREATE_FRAME(
type: HEADERS,
streamID: streamID,
flags: END_HEADERS,
payload: HPACK_ENCODE(request.headers)
)
SEND_FRAME(connection, headersFrame)
IF request.body is not empty THEN
dataFrame <- CREATE_FRAME(
type: DATA,
streamID: streamID,
flags: END_STREAM,
payload: request.body
)
SEND_FRAME(connection, dataFrame)
END IF
activeStreams[streamID] <- request
END FOR
// Receive response frames (interleaved from different streams)
WHILE LENGTH(responses) < LENGTH(pendingRequests) DO
frame <- RECEIVE_FRAME(connection)
IF frame.type = HEADERS THEN
responses[frame.streamID] <- NEW_RESPONSE()
responses[frame.streamID].headers <- HPACK_DECODE(frame.payload)
ELSE IF frame.type = DATA THEN
APPEND frame.payload TO responses[frame.streamID].body
END IF
IF frame.flags CONTAINS END_STREAM THEN
MARK_COMPLETE(responses[frame.streamID])
REMOVE activeStreams[frame.streamID]
END IF
END WHILE
RETURN responses
END
ALGORITHM TLS13Handshake(client, server)
INPUT: client: TLS client, server: TLS server
OUTPUT: encrypted connection with shared secret
BEGIN
// Client Hello (1 message)
clientRandom <- GENERATE_RANDOM(32)
clientKeyShare <- GENERATE_ECDHE_KEYPAIR()
clientHello <- CREATE_MESSAGE(
type: CLIENT_HELLO,
random: clientRandom,
cipherSuites: [TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256],
keyShares: [clientKeyShare.publicKey],
supportedVersions: [TLS_1_3]
)
SEND(client, server, clientHello)
// Server Hello + encrypted data (1 flight)
serverRandom <- GENERATE_RANDOM(32)
serverKeyShare <- GENERATE_ECDHE_KEYPAIR()
// Compute shared secret using ECDHE
sharedSecret <- ECDHE(serverKeyShare.privateKey, clientKeyShare.publicKey)
handshakeKeys <- DERIVE_KEYS(sharedSecret, clientRandom, serverRandom)
serverHello <- CREATE_MESSAGE(
type: SERVER_HELLO,
random: serverRandom,
cipherSuite: TLS_AES_256_GCM_SHA384,
keyShare: serverKeyShare.publicKey
)
SEND(server, client, serverHello)
// Encrypted with handshake keys
SEND_ENCRYPTED(server, client, handshakeKeys,
encryptedExtensions: server.extensions,
certificate: server.certificate,
certificateVerify: SIGN(server.privateKey, handshakeTranscript),
finished: HMAC(handshakeKeys.serverFinished, handshakeTranscript)
)
// Client verifies and finishes
VERIFY_CERTIFICATE(server.certificate, trustedCAs)
VERIFY_SIGNATURE(server.certificate.publicKey, certificateVerify, handshakeTranscript)
clientSharedSecret <- ECDHE(clientKeyShare.privateKey, serverKeyShare.publicKey)
applicationKeys <- DERIVE_APPLICATION_KEYS(clientSharedSecret)
SEND_ENCRYPTED(client, server, handshakeKeys,
finished: HMAC(handshakeKeys.clientFinished, handshakeTranscript)
)
// Connection established with application keys
RETURN ENCRYPTED_CONNECTION(applicationKeys)
END
ALGORITHM WebSocketHandshakeAndCommunicate(client, server, path)
INPUT: client: WebSocket client, server: target server, path: endpoint path
OUTPUT: bidirectional message channel
BEGIN
// Step 1: HTTP Upgrade request
wsKey <- BASE64_ENCODE(GENERATE_RANDOM(16))
upgradeRequest <- CREATE_HTTP_REQUEST(
method: GET,
path: path,
headers: {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-WebSocket-Key": wsKey,
"Sec-WebSocket-Version": "13"
}
)
SEND_HTTP(client, server, upgradeRequest)
// Step 2: Server responds with 101
response <- RECEIVE_HTTP(server)
IF response.status != 101 THEN
RETURN ERROR("WebSocket upgrade failed")
END IF
expectedAccept <- BASE64_ENCODE(SHA1(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
IF response.headers["Sec-WebSocket-Accept"] != expectedAccept THEN
RETURN ERROR("Invalid accept key")
END IF
// Step 3: Connection is now WebSocket, full-duplex messaging
channel <- NEW_WEBSOCKET_CHANNEL(client, server)
// Either side can send at any time
RETURN channel
END
ALGORITHM SendWebSocketFrame(channel, message, isClient)
INPUT: channel: WebSocket connection, message: data to send, isClient: boolean
OUTPUT: frame sent
BEGIN
frame <- NEW_FRAME()
frame.fin <- true // final fragment
frame.opcode <- IF IS_TEXT(message) THEN 0x1 ELSE 0x2 // text or binary
IF LENGTH(message) <= 125 THEN
frame.payloadLength <- LENGTH(message)
ELSE IF LENGTH(message) <= 65535 THEN
frame.payloadLength <- 126
frame.extendedLength <- LENGTH(message) // 2 bytes
ELSE
frame.payloadLength <- 127
frame.extendedLength <- LENGTH(message) // 8 bytes
END IF
IF isClient THEN
frame.mask <- true
frame.maskingKey <- GENERATE_RANDOM(4)
frame.payload <- XOR_MASK(message, frame.maskingKey)
ELSE
frame.mask <- false
frame.payload <- message
END IF
SEND_BYTES(channel, SERIALIZE(frame))
END
Real-World Applications
- Modern web applications: virtually all major websites use HTTP/2 over TLS; the combination of multiplexing and header compression reduces page load times by 15-50% compared to HTTP/1.1, especially on high-latency mobile connections
- Real-time collaboration: tools like Google Docs, Figma, and Notion use WebSockets to synchronize edits between users in real time; each keystroke or cursor movement is sent as a small WebSocket message
- Financial trading platforms: stock tickers and trading interfaces use WebSockets to push price updates to clients with minimal latency; the persistent connection avoids the overhead of repeated HTTP requests
- Chat and messaging: Slack, Discord, and WhatsApp Web use WebSockets for instant message delivery; the server pushes new messages to connected clients without polling
- API gateways: services like Cloudflare, AWS API Gateway, and Nginx terminate TLS at the edge, handling certificate management and encryption offloading so backend services can communicate over plain HTTP internally
- gRPC: Google's RPC framework runs over HTTP/2, using its multiplexing and binary framing to support efficient bidirectional streaming between microservices
- Certificate transparency: the web PKI ecosystem uses Certificate Transparency logs to detect misissued certificates, with browsers requiring certificates to include Signed Certificate Timestamps (SCTs) from multiple logs
Key Takeaways
- HTTP/2 multiplexes multiple request-response streams over a single TCP connection, eliminating HTTP-level head-of-line blocking and reducing the overhead of multiple connections
- HPACK header compression reduces repeated header overhead by 95-98%, encoding common headers as small indices into a shared dynamic table
- TLS 1.3 completes the handshake in 1 RTT (down from 2 in TLS 1.2) and supports 0-RTT resumption for returning clients; it exclusively uses ephemeral key exchange (ECDHE) for perfect forward secrecy
- WebSockets provide full-duplex, bidirectional communication with minimal per-message overhead (2-10 bytes), enabling real-time applications that HTTP's request-response model cannot efficiently support
- The total time to first byte for a new HTTPS connection is $t_{\text{DNS}} + \text{RTT}_{\text{TCP}} + \text{RTT}_{\text{TLS}} + \text{RTT}_{\text{HTTP}} + t_{\text{server}}$, making connection reuse and 0-RTT resumption critical for performance
- HTTP/2 still suffers from TCP-level head-of-line blocking (a lost TCP packet stalls all streams); HTTP/3 (QUIC) solves this by running over UDP with per-stream loss recovery