HTTP/2 Streams Visualizer
HTTP/2 multiplexing sends many requests over one TCP connection. Each stream carries interleaved frames weighted by priority; the server allocates bandwidth proportionally. One handshake, no head-of-line blocking at the application layer.
Each row is one HTTP/2 stream on a single shared connection. The id is its stream number, w is its priority weight, and the fill bar shows bytes sent against the stream's total. Every Tick hands out a fixed 100-byte budget and splits it across the unfinished streams in proportion to their weights, which is how the server's prioritiser decides who moves next. Auto runs the ticks for you; the counters up top track how many streams are open and how many are still in flight.
Hit Auto and watch all four bars advance at once, not one after another. That is multiplexing: a slow, heavy stream and a light one share the same pipe instead of queueing. Then add a stream with weight 16 mid-run and notice the others slow down as their share of the budget shrinks, and a finished stream hands its share back to the rest instantly. What should surprise you is that weight buys a proportion of bandwidth, not a turn in line. Double a weight and that stream gets roughly double the bytes per tick while everyone keeps moving.
What are HTTP/2 streams?
Six pipes, all clogged at once.
HTTP/2 streams are independent, bidirectional sequences of frames inside a single TCP connection. HTTP/2 (RFC 7540, 2015) replaced HTTP/1.1's six-connection-per-origin browser default with one connection carrying many streams, each with its own flow-control window. SPDY at Google was the predecessor; HTTP/3 (RFC 9114, 2022) takes the same idea over QUIC.
Imagine the browser loading a moderately busy news site. The HTML names a hundred and twenty separate resources — stylesheets, scripts, web fonts, images, third-party trackers, an embedded video player, a chat widget. Under HTTP/1.1, the browser opens up to six TCP connections to the origin and races those resources down them. Six is a polite limit baked into every browser since the late nineties. The other hundred and fourteen resources wait in a queue.
This produces a cascade of small problems that together feel like a slow page. Each TCP connection costs a TLS handshake — three round trips on a cold start, one or two on resumption — so opening six of them is essentially three hundred milliseconds of pure setup before anything useful flows. Each connection is also strictly ordered: a request for a slow resource at the front of the queue blocks every faster request behind it on the same connection. The server cannot deliver bytes for a small CSS file until the big JPEG ahead of it has finished. The polite name for this is head-of-line blocking. The exhausting name for it is "why is the spinner still spinning when I have gigabit."
HTTP/2 is the protocol that throws all of that out and asks one different question: what if every request and every response shared a single connection, with bytes interleaved frame by frame, and the server got to choose what to send next? Then six handshakes become one. Head-of-line blocking at the application layer disappears, because every byte carries a stream identifier and the receiver re-assembles per stream. Headers compress against a shared dictionary so the third request for the same origin costs almost nothing. The server can prioritise: send the CSS that blocks rendering ahead of the analytics pixel that doesn't.
Concretely: a typical page with HTTP/1.1 on a fast home connection finishes in about 2.4 seconds. The same page over HTTP/2 finishes in about 1.3. Telemetry from Cloudflare, Google, and Akamai through 2015 to 2017 was unanimous; the pattern was so consistent that browsers stopped offering HTTP/2 as opt-in and started preferring it by default. The simulator above is the smallest possible window onto that win: many streams sharing one socket, the server allocating bandwidth by weight, frames interleaving in real time. Drag a weight up and you steal share from the others; cancel a stream and the rest absorb the freed bandwidth instantly.
Streams as channels in a single TCP connection
Streams are channels in a single pipe.
A useful mental model: think of HTTP/1.1 as a row of single-lane country roads. To send three trucks at once you need three roads. If a truck on one road breaks down, the trucks behind it on that road wait. Adding more roads is expensive and there's a cap on how many your house can have driveways for.
HTTP/2 is one wide motorway with painted lanes. Every truck still belongs to a labelled lane (its stream), but the pavement is shared and the road operator gets to decide which truck moves forward at each moment. A slow truck doesn't block a fast one — they just travel side by side. A breakdown on lane four doesn't bring lanes one through three to a halt at the application layer, because the receiver sorts each delivered chunk back into its lane on arrival.
The "frames" you see referenced in the simulator and elsewhere are these per-lane chunks. A frame is a small unit — at most sixteen kilobytes by default, and often much less — with a header that names the stream, the type (HEADERS, DATA, PRIORITY, and so on), and a length. The server emits frames in whatever order best uses the connection. Stream three's HEADERS, stream one's DATA, stream three's DATA, stream five's HEADERS, more stream one DATA, and so on. The receiver reassembles each stream from its labelled frames; the streams complete in any order.
Two practical numbers anchor the win. A typical HTTP/2 connection holds dozens to hundreds of concurrent streams; the simulator's four streams are a tiny slice. And HPACK header compression collapses repeat headers from around a kilobyte each to one or two bytes by sharing a table of seen values across both ends. On a page with two hundred requests, that's two hundred kilobytes of redundant header bytes that simply don't travel. The protocol's wins compound: fewer handshakes, smaller headers, no app-layer blocking, prioritisation. They are individually small. They add up to noticeable seconds.
Origins — SPDY at Google, HTTP/2 at the IETF
A protocol born at Google.
HTTP/2 began life as SPDY, an experimental protocol Mike Belshe and Roberto Peon proposed at Google in late 2009. The motivating observation was simple and uncomfortable: HTTP/1.1 had not changed meaningfully since 1997, the web's per-page request count had multiplied tenfold, and the cost of opening one TCP connection per resource — or even six per origin, the polite browser limit — was the dominant chunk of a typical page load. SPDY combined four ideas. Multiplex many requests over one TCP connection. Compress headers, which had ballooned to several kilobytes per request. Let the server prioritise. And let the server push resources the client had not yet asked for. Google rolled SPDY into Chrome and onto google.com in 2011; Twitter, Facebook, and a few CDNs followed.
In 2012 the IETF chartered the httpbis working group to standardise the next version. SPDY/3 was the starting point. The result, after three years of revisions, was RFC 7540 in May 2015 — HTTP/2 — alongside its companion compression spec, RFC 7541 for HPACK. Both were authored by Mike Belshe (Twist), Roberto Peon (Google), and Martin Thomson (Mozilla). RFC 7540 was superseded in June 2022 by RFC 9113, edited by Thomson and Cory Benfield, which folded in errata, removed the deprecated dependency-based PRIORITY frame, and clarified extensibility points. RFC 9113 is the current canonical spec. The wire format is unchanged from 7540; the deprecations are the meaningful difference.
By 2026, HTTP/2 carries an estimated 35–40% of all web traffic, sitting between HTTP/1.1 (still roughly half of long-tail traffic) and HTTP/3 over QUIC (rising past 25% on major CDNs). Cloudflare, Akamai, Fastly, Google, and AWS all enable HTTP/2 by default for any TLS-terminated origin; Apache (mod_http2 since 2.4.17, October 2015), NGINX (since 1.9.5, also October 2015), Microsoft IIS (Server 2016), Envoy from inception, H2O from inception, and the Go net/http package since 1.6 all ship HTTP/2 in the box. Compliance is verified by the h2spec test suite (Moto Ishizawa, 2015), which every serious implementation runs in CI.
One curious historical footnote: the spec mandates a magic preface — the literal bytes PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n — that any HTTP/2 client must send before any frames. The string spells "PRISM" if you read the first letters of the lines, an Edward Snowden joke buried in the protocol by the working group at the height of the surveillance disclosures. The preface itself is a defence: a real HTTP/1.1 server seeing those bytes will respond with a 400 or 405, telling the client unambiguously that the upstream cannot speak HTTP/2.
HTTP/2 frame types — ten frames, one connection
Ten frame types, one connection.
HTTP/2 replaces HTTP/1.1's text framing with binary frames. Every frame has a 9-byte header — 24 bits of length, 8 bits of type, 8 bits of flags, then a 31-bit stream identifier with one reserved bit — followed by a typed payload of up to SETTINGS_MAX_FRAME_SIZE bytes (default 16384, configurable up to 16 MiB minus one). Ten frame types do all the work; everything else is reserved for extensions. The fixed nine-byte header means parsing is allocation-free in any sensible implementation: read nine bytes, branch on the type byte, dispatch to the typed parser.
| Frame | What it carries |
|---|---|
| HEADERS | Request/response headers, HPACK-compressed. Opens a stream. |
| DATA | Body bytes for a stream. Subject to flow control. |
| PRIORITY | Stream weight + dependency. Deprecated in RFC 9113; replaced by RFC 9218 PRIORITY_UPDATE. |
| RST_STREAM | Cancel a stream with an error code. Per-stream abort. |
| SETTINGS | Connection-wide knobs: max streams, initial window size, header table size. |
| PING | Round-trip-time measurement and keep-alive. |
| GOAWAY | Graceful connection shutdown — "no new streams past this ID". |
| WINDOW_UPDATE | Flow control credit per stream + connection. |
| CONTINUATION | Header continuation when one HEADERS frame won't fit. |
| PUSH_PROMISE | Server-initiated stream. Disabled in Chrome since 2022; almost no real use. |
Stream identifiers carry meaning of their own. Stream zero is reserved for connection-level control — SETTINGS, PING, GOAWAY, and WINDOW_UPDATEs that adjust the connection-wide receive window all live there. Odd-numbered streams are client-initiated (the client opens the first request as stream 1, the second as 3, the third as 5, and so on). Even-numbered streams are server-initiated, which historically meant PUSH_PROMISE responses; with server push deprecated, even stream IDs are nearly extinct outside test suites.
Stream IDs only ever increase, never wrap around or reuse. Once a connection has used stream 16777215, it must close and a new connection must open. In practice no one hits this limit — even at one new stream per millisecond a connection would last 4.6 hours before exhausting IDs — but the unidirectional ratchet matters because it lets GOAWAY refer to "all streams up to this ID are still valid; everything later is rejected" without ambiguity. A connection holds thousands of concurrent streams; they multiplex frame-by-frame, take their flow-control window, and finish in any order.
HTTP/2 frame types — ten frames, one connection
Ten frame types, one connection.
HTTP/2 has its own flow control, layered on top of TCP's. Each peer announces a receive window for every stream and an additional connection-level receive window; the sender of DATA frames may not transmit more bytes than the receiver has credited. The default initial window is 65535 octets per stream (the value of SETTINGS_INITIAL_WINDOW_SIZE), inherited unchanged from SPDY. As the receiver consumes data, it returns credit by sending WINDOW_UPDATE frames; the sender adds the credit to its tracking and continues.
Flow control matters because it lets a slow consumer push back on a fast producer without dropping the connection. A browser fetching a 1 GB video does not need to receive all 1 GB in flight; it advertises a small window, the server fills it, the browser plays back, returns credit, the server fills more. TCP does this at the connection level; HTTP/2 does it again per stream so a single greedy stream can not starve siblings.
The 65535-octet default is a relic. On a high-bandwidth-delay-product link — a 1 Gbps connection with 50 ms RTT has a bandwidth-delay product of roughly 6 MB — a single 64 KB window allows at most 65535 / 0.05 = 1.3 MB/s per stream. Every modern client and server overrides the default in its first SETTINGS frame: Chrome announces a 6 MB window, NGINX defaults to 256 KB and is usually tuned higher, Envoy defaults to 256 MB. If a server fails to raise its initial window or fails to send WINDOW_UPDATEs promptly, throughput collapses on long-haul links and engineers spend a frustrating afternoon discovering that their gigabit pipe is delivering megabits.
SETTINGS frames carry a handful of other knobs. SETTINGS_MAX_CONCURRENT_STREAMS caps how many streams can be open at once (default unlimited; Chrome and Firefox cap themselves at 100; servers usually cap clients at 100–250 to bound memory). SETTINGS_MAX_FRAME_SIZE is the largest frame either side will send. SETTINGS_HEADER_TABLE_SIZE bounds the HPACK dynamic table (default 4096 bytes). SETTINGS_ENABLE_PUSH set to zero opts out of server push entirely — which Chrome announces by default since 2022.
A subtle interaction with kernel TCP buffers is worth flagging. The HTTP/2 receive window controls how many bytes the application is willing to accept; the kernel's SO_RCVBUF controls how many the socket layer will buffer. If the HTTP/2 window is much larger than the kernel buffer, the kernel back-pressures TCP and your generous HTTP/2 advertisement is a polite fiction. If the kernel buffer is much larger than the HTTP/2 window, you accept bytes you cannot dispatch and waste memory. Sensible practice is to keep the two roughly aligned and to autotune both — net.ipv4.tcp_rmem on Linux defaults to a max of 6 MB on modern kernels, which is a reasonable upper bound for the HTTP/2 window as well. Brendan Gregg's USE-method dashboards are a good first stop when chasing throughput regressions on the connection.
# SETTINGS frame · type 0x04 · sent first by both peers # Layout: 9-byte frame header, then N × 6-byte (id, value) pairs +-----------------------------------------------+ | Length (24) | Type=0x04 (8) | +---------------+-------------------------------+ | Flags (8)=ACK?| R | Stream Identifier=0 | +---------------+-------------------------------+ | Identifier (16) | Value (32) | ← repeats +-----------------------------------------------+ # Identifiers 0x01 HEADER_TABLE_SIZE 4096 (HPACK dynamic table) 0x02 ENABLE_PUSH 0/1 (clients announce 0) 0x03 MAX_CONCURRENT_STREAMS 100-250 (cap per peer) 0x04 INITIAL_WINDOW_SIZE 65535 (raise on long-haul) 0x05 MAX_FRAME_SIZE 16384 (up to 16777215) 0x06 MAX_HEADER_LIST_SIZE unset (advisory cap)
HPACK — headers that repeat for free
Headers that repeat for free.
A typical web page issues 80 to 200 requests during initial load, each carrying the same User-Agent, Accept, Cookie, and several hundred bytes of CDN-injected headers. HTTP/1.1 transmitted every byte every time. HPACK (RFC 7541, May 2015, Roberto Peon and Herve Ruellan) compresses this to one or two bytes per repeat by maintaining synchronised tables on both ends of the connection.
Two tables drive the encoding. The static table is 61 entries hard-coded into the spec, populated with the headers measured most common in real HTTP traffic when SPDY was being designed: :method GET at index 2, :status 200 at index 8, accept-encoding gzip, deflate at index 16. The dynamic table grows as headers stream over the connection — every HEADERS frame may add or evict entries — and both peers track it identically. Once a header value has been transmitted once, subsequent occurrences are referenced by table index, costing as little as a single byte.
Request 1: User-Agent: Mozilla/5.0 … → adds entry to dynamic table at index 62.
Request 2: same User-Agent → wire bytes are [0xBE] (1 byte: "literal indexed name & value, index 62"). 200-byte header collapsed to 1 byte.
HPACK uses a static Huffman code (also defined in RFC 7541, distinct from the table) to compress the literal portions when they don't yet appear in either table. The code is tuned to typical ASCII header content; it shaves another 20–30% off literal bytes at minimal CPU cost. End-to-end, real-world page loads see header overhead drop from typical ranges of 800–1500 bytes per request to 30–80 bytes — a 10–30× reduction. On mobile connections, where every kilobyte saved is a noticeable fraction of round-trip latency, this dominates the user-visible improvement.
HPACK has a known weakness: CRIME-style compression-oracle attacks (Juliano Rizzo and Thai Duong, 2012) can leak secrets when an attacker can mix attacker-controlled and victim-controlled data into the same compressed stream. The HPACK mitigation — never share a dynamic table across security contexts, and let either end refuse to index sensitive headers via the Never Indexed bit — is built into every compliant implementation. HTTP/3 keeps the same threat model and uses QPACK (RFC 9204), a redesign that splits encoder and decoder tables to handle QUIC's out-of-order delivery without sacrificing the security properties.
HTTP/2 features that did not survive — Server Push, prioritisation
Two features that did not survive.
RFC 7540 shipped two features that real implementations either could not implement well or did not benefit from. Both have been formally deprecated. The first is the dependency-based priority tree: streams could declare a parent stream and a weight, forming a tree, and the server was expected to allocate bandwidth proportionally to weight among siblings while preferring children of the most-recently-finished parent. The model is elegant on paper. In practice, browsers built three different incompatible policies for constructing the tree, servers built three different incompatible scheduling algorithms for executing it, and the resulting matrix produced page-load behaviour that was worse than first-come-first-served on benchmarks. Daniel Stenberg (curl) and Patrick Meenan (Cloudflare) documented the failure publicly in 2018; the working group replaced the model entirely with RFC 9218 Extensible Prioritization in 2022, a much simpler scheme that uses two HTTP headers (priority: u=<urgency>, i) and lets servers take it as advisory.
The second is server push. PUSH_PROMISE let a server pre-emptively send resources alongside the response that referenced them — push the CSS while sending the HTML. The mechanic is sound; the cache integration is not. The server has to guess what the client already has cached; if it pushes resources the client already has, it wastes bandwidth and bloats the page-load critical path. Telemetry from Chrome, Akamai, and Cloudflare in 2021 showed server push consistently failing to improve, and often actively harming, user-visible page-load metrics. Chrome disabled server push by default in May 2022 (release 106); Cloudflare followed soon after. PUSH_PROMISE remains in the spec but is functionally extinct, replaced by 103 Early Hints (RFC 8297) where a server returns hints about resources without actually shipping them.
A third feature did not get cut but did get re-evaluated: connection coalescing. HTTP/2 allows a single connection to serve multiple origins as long as the TLS certificate covers all of them. Browsers exploit this aggressively — example.com and cdn.example.com served from the same load balancer with a wildcard cert share one TCP connection. Coalescing reduces handshakes; it also concentrates head-of-line blocking. As HTTP/3 has eliminated the underlying TCP HOL problem, coalescing has become unambiguously useful again, and Chrome's networking stack now coalesces over QUIC by default.
The lesson is unglamorous but real. Protocols ship features that look good in design and work poorly under deployment pressure. The features get used regardless because they are shipped. Telemetry eventually catches up; deprecation follows; the spec evolves. RFC 9113 is the post-deprecation HTTP/2; RFC 7540 is best read for historical context.
Multiplexing's hidden cost — TCP head-of-line blocking
Multiplexing's hidden cost.
HTTP/2 solved application-level head-of-line blocking — slow stream A no longer holds up fast stream B at the HTTP layer. But HTTP/2 still rides on TCP, and TCP delivers bytes in order regardless of which stream they belong to. A single dropped IP packet on stream A stalls every byte after it in the receive buffer, including bytes destined for streams B, C, and D, until the retransmission arrives. The HOL problem moved one layer down, where it is harder to see and impossible to fix without leaving TCP.
On a clean link the cost is invisible. On a flaky cellular network with 1% packet loss, an HTTP/2 page can load slower than the same page over HTTP/1.1's six parallel TCP connections. Each H/1.1 connection has its own loss recovery; the problem isn't shared. Robin Marx's measurement work at Hasselt University (2018–2020) put numbers on this: HTTP/2 outperforms HTTP/1.1 on loss rates below ~0.5% and is consistently slower above ~2%. The cellular average sits awkwardly in between.
HTTP/3 over QUIC fixes this for real. QUIC, designed at Google starting in 2012 and standardised as RFC 9000 in 2021, moves stream framing into the transport layer. Each QUIC stream has its own ordering and its own retransmit queue; a lost packet for stream A only stalls stream A. Bonus: QUIC sits on UDP (so middleboxes cannot inspect it without explicit support), the connection ID is independent of IP (so a Wi-Fi to LTE switch does not reset the connection), and TLS 1.3 is integrated rather than layered (one round-trip handshake, with 0-RTT for resumption). HTTP/3 is RFC 9114, also from June 2022; it reuses QPACK from RFC 9204.
Move stream framing out of the application layer and into the transport, so each stream gets its own loss recovery, its own ordering, and its own back-pressure — and the TCP head-of-line problem stops being shared.
There is one practical wrinkle. QUIC's per-stream encryption forces every implementation to do per-packet AES or ChaCha20 work in user space (kernels rarely offer QUIC offload yet), so on a CPU-bound load balancer QUIC may cost 1.5–2× the cycles of HTTP/2 over kernel TLS for the same throughput. Cloudflare's quiche, Google's quiche (different code base, same name), Microsoft's MsQuic, the Rust ecosystem's quinn, and the Linux kernel's nascent io_uring-based path are all chasing this gap. For now, HTTP/2 over TCP remains the more economical choice on cycle-constrained edge nodes; HTTP/3 dominates wherever the bottleneck is RTT or loss.
Tuning, debugging, and staying upright with HTTP/2
Tuning, debugging, staying upright.
Operating HTTP/2 at scale is mostly about three knobs and three failure modes. The knobs: initial window size (raise it on long-haul links), max concurrent streams (cap it on the server to bound memory; raise it on the client to allow parallelism), and max frame size (raise it for bulk transfers, leave it at 16 KB for low-latency interactive traffic). The failure modes: stream exhaustion (slow leaks of unfinished streams hitting SETTINGS_MAX_CONCURRENT_STREAMS), window starvation (a misbehaving stream drains the connection-level window and stalls everything else), and RST_STREAM amplification (the topic of CVE-2023-44487, the "HTTP/2 Rapid Reset" attack disclosed by Google, AWS, and Cloudflare in October 2023, which exploited cheap stream cancellations to overwhelm servers; mitigations now ship in every major implementation).
Debugging differs from HTTP/1.1. curl --http2 -v shows the negotiation but does not show frames. nghttp -v from the nghttp2 toolkit (Tatsuhiro Tsujikawa, the de-facto reference C implementation) prints frame-by-frame dumps. Wireshark has had an HTTP/2 dissector since 1.99.7 (2015); it decodes HEADERS via HPACK if you give it the keys. Chrome's chrome://net-export and Firefox's about:networking dump per-frame events. Server-side, NGINX's debug logs name every frame; Envoy's --component-log-level http2:trace does the same.
Operationally: turn on HTTP/2 by default at every TLS-terminating edge. Disable server push (it is not helping). Tune your initial window for the bandwidth-delay product of your worst paying customer, not your average. Cap SETTINGS_MAX_CONCURRENT_STREAMS to a small number (100–250) on public-facing servers; this is the single most effective Rapid Reset mitigation. Run h2spec in CI for any HTTP/2 server you write or modify. Monitor connection age and stream churn; sudden spikes are usually a client behaving badly.
HTTP/2 will live alongside HTTP/3 for years. The transition is gradual; QUIC requires UDP, which a non-trivial fraction of corporate firewalls still block, and HTTP/2 over TCP is the universal fallback. A modern serving stack should answer in HTTP/3, fall back to HTTP/2, and finally to HTTP/1.1, advertising each via the Alt-Svc header so clients can upgrade in place. Cloudflare, Google, Apple, and Akamai all already do this. The protocol stack is more layered than ever; the operator's job is to make sure each layer is tuned and observable, not to pick one.
Further reading on HTTP/2
Primary sources, in order.
- IETF · RFC 9113HTTP/2Frame types, settings, priority, flow control. The current canonical spec — supersedes RFC 7540 with errata folded in and dependency-based priority deprecated.
- IETF · RFC 7541HPACK · Header Compression for HTTP/2The static and dynamic tables behind Part 04. Short, formal, surprisingly readable; the Huffman code appendix is the best part.
- IETF · RFC 9114HTTP/3HTTP/2's frame model rebuilt on QUIC. Read after Part 06 for the post-TCP picture; pairs with RFC 9000 (QUIC) and RFC 9204 (QPACK).
- IETF · RFC 9218Extensible Prioritization Scheme for HTTPThe replacement for the deprecated PRIORITY frame. Two headers, server-advisory, much simpler.
- Cloudflare blog · 2020Head-of-line blocking in QUIC and HTTP/3, the detailsRobin Marx's deep dive on the same Part 06 phenomenon, with measurements and surprising places it still bites.
- Daniel Stenberg · 2018HTTP/2 and the pesky prioritiesWhy the original PRIORITY frame failed in practice, and what RFC 9218 PRIORITY_UPDATE replaced it with.
- Cloudflare · 2023HTTP/2 Rapid Reset (CVE-2023-44487)The amplification attack and the mitigations. Required reading for anyone running an HTTP/2 server.
- Daniel Stenberg · bookHTTP/3 explainedA free book by the curl maintainer. Beginner-friendly walkthrough of how HTTP/2 evolved into HTTP/3 and why each piece exists.
- Ilya Grigorik · 2013High Performance Browser NetworkingO'Reilly book, fully online. Chapters 11–13 cover HTTP/2 framing, multiplexing, and prioritisation in depth. The standard student introduction.
- NGINX blogIntroducing HTTP/2 server push with NGINX 1.13.9Production-side perspective on tuning the receive window, max concurrent streams, and the operational hooks that matter at scale.
- Tatsuhiro Tsujikawa & Moto Ishizawah2spec — conformance test suiteThe reference test runner every serious HTTP/2 server runs in CI. Reading the test descriptions is itself a tour of the spec.
- Semicolony simulatorTCP handshakeThe three-way handshake whose cost HTTP/2 amortises across many requests.
- Semicolony guideHTTP, picked apartEvery version. What each one tried to make faster, and what it gave up to do so.