HTTP is not duplex (the Spur mistake)
Why HTTP is half-duplex, what duplex actually means at each layer, and the protocols that fill the gap.
Duplex defined
Three terms get confused: simplex, half-duplex, full-duplex.
- Simplex: data flows one way only. Like a radio broadcast.
- Half-duplex: data flows both ways but only one direction at a time. Like a walkie-talkie.
- Full-duplex: data flows both ways simultaneously. Like a phone call.
At each layer of the stack, you can ask "is this layer full-duplex?" The answer is sometimes yes for the lower layer and no for the upper layer.
The transport layer is full-duplex
TCP is full-duplex. The standard says so explicitly. Each TCP connection has two independent byte streams: client-to-server and server-to-client. Both can carry data concurrently. The kernel keeps separate send and receive buffers per direction.
QUIC is also full-duplex. Each QUIC stream can be bidirectional, with independent flow control per direction.
So at L4, the answer is "yes, full-duplex."
HTTP semantics are half-duplex
HTTP semantics, defined in RFC 9110, are strictly request-response. The client sends a request, the server sends a response. The lifecycle of an HTTP message exchange is:
- Client opens a connection (or reuses one).
- Client writes request: method, path, headers, optional body.
- Server writes response: status, headers, optional body.
- Either side may close (or keep alive for next request).
There is no protocol mechanism for the server to send a message that is not a response to a request. Even HTTP/2 server push (which is now deprecated) is technically a pushed response to a synthesized request.
This is the half-duplex part: at any moment, one side is speaking. The roles are well-defined: client requests, server responds. They do not interleave.
HTTP/2 streams are not full-duplex either
HTTP/2 introduced multiplexed streams. Multiple requests can be in flight on the same TCP connection. So is HTTP/2 full-duplex?
Subtle: each stream is request-response. But across streams, request frames for stream 5 and response frames for stream 3 can interleave on the wire. The connection is bidirectional in terms of byte flow, but each stream is still client-initiates, server-responds.
The exception: gRPC bidi streaming uses HTTP/2's DATA frames in both directions on the same stream, with request body and response body flowing concurrently. This is genuinely full-duplex at the stream level. But it requires:
- Client sends HEADERS to start the stream.
- Server sends HEADERS to acknowledge.
- Both can send DATA frames concurrently until one sends END_STREAM.
Notice the asymmetry: client always initiates. The server cannot start a new stream and push events on its own (server push aside).
HTTP/3 is the same story
HTTP/3 runs over QUIC. QUIC streams are full-duplex at the transport level. HTTP/3 semantics inherit from HTTP/2: still request-response per stream, still client-initiated.
So upgrading from HTTP/2 to HTTP/3 does not magically make HTTP duplex. The transport gets better; the application protocol semantics do not change.
The Spur interview, in detail
This kind of question separates engineers who learned protocols from those who learned APIs. If you only ever called fetch(), you might think the network is just request-response forever. If you have ever written a real-time app, you know request-response is one of many shapes.
The protocols that fill the duplex gap
WebSocket
WebSocket starts as an HTTP/1.1 GET with Upgrade: websocket. The server responds with 101 Switching Protocols. After that, the TCP connection carries WebSocket framing in both directions, fully symmetric.
After upgrade:
- Both client and server send messages at any time.
- Messages are framed (text or binary), with masking from client side.
- One TCP connection, full-duplex, no request-response semantics.
Use for: chat, collaborative editing, live dashboards, multiplayer games.
Server-Sent Events (SSE)
SSE is a streaming HTTP response. The client makes a GET, server keeps the response open and sends event lines:
data: {"type": "user_joined", "id": 42}
data: {"type": "message", "text": "hi"}
One direction only: server to client. The client can issue another request on a separate connection if it needs to talk back.
Use for: server notifications, live feeds, server-pushed updates where the client does not need to reply on the same channel.
gRPC bidi streaming
Over HTTP/2 (or HTTP/3), gRPC opens a stream and both sides send messages. The protocol calls this "bidirectional streaming." This is genuinely full-duplex per stream.
Use for: voice transcription, real-time RPC, anything that fits a typed-stream model.
Long polling
The client makes a GET. Server holds the response open until it has data, then responds. Client immediately makes another GET. This fakes server push using only HTTP.
Use for: graceful fallback when WebSocket is blocked by a corporate firewall. Most chat apps still support long polling as a fallback.
HTTP/2 server push (RIP)
Server preemptively sends responses to requests the client has not made yet. Was supposed to make HTML+CSS+JS round trips disappear. In practice, cache-busted in confusing ways and was hard to tune. Chrome removed support in 2022. Use 103 Early Hints instead.
103 Early Hints
A 103 status code lets the server send Link: preload hints before the final response. The browser can start fetching CSS and JS while the server computes the HTML. This is not a push; it is a polite "you will want these."
Same use case as server push, less foot-gun.
Why HTTP stayed half-duplex
HTTP was designed for documents. The web in 1991 was static pages. Request-response fit perfectly.
Adding bidirectional to HTTP would have required:
- Asymmetric connection state (who is initiating right now?).
- Stream IDs at the connection layer (only added in HTTP/2).
- Reframing of every intermediary (proxies, CDNs).
The web grew faster than HTTP could evolve. WebSocket appeared in 2011 as the answer: keep HTTP's handshake (so it tunnels through proxies) but switch to a different framing afterward.
What this means for interview answers
"Is HTTP duplex?" Answer: HTTP is half-duplex. The semantic model is client-requests-then-server-responds. The transport beneath is full-duplex but HTTP does not expose that. For real bidirectional, use WebSocket or gRPC streaming.
"How would you build a chat app?" Answer: WebSocket for live messages, with long polling as fallback. HTTP REST for history, login, profile.
"Why can't I just push from server with HTTP?" Answer: HTTP has no concept of server-initiated messages. The closest things are HTTP/2 server push (deprecated) and 103 Early Hints (limited to resource preload hints).
Layer-by-layer summary table
| Layer | Protocol | Duplex |
|---|---|---|
| Physical | copper, fiber | full |
| L2 | Ethernet | full |
| L3 | IP | per-packet, no concept |
| L4 | TCP | full |
| L4 | QUIC | full |
| L7 | HTTP | half (request-response) |
| L7 | WebSocket | full |
| L7 | gRPC bidi | full per stream |
| L7 | SSE | simplex (server to client) |
The architectural lesson
Half-duplex is not a bug; it is a design choice. Request-response is easy to reason about, easy to cache, easy to proxy. Full-duplex is harder: connection state on both sides, head-of-line concerns, harder to scale horizontally because sticky sessions matter.
Most APIs do not need full-duplex. REST endpoints, GraphQL queries, file downloads are all request-response. Reach for WebSocket or SSE only when you have a genuine push requirement.
Learn more
- Paper
- Paper
- PaperRFC 9113: HTTP/2IETF
- Docs
- DocsgRPC over HTTP/2gRPC