HTTP/2 is the second major version of HTTP, defined today by RFC 9113 (June 2022). It keeps the same methods, status codes and headers as HTTP/1.1 but sends them as binary frames, runs many requests in parallel as streams over one TCP connection, and compresses headers with HPACK (RFC 7541). Browsers use it over TLS, negotiated during the handshake.
That design removes the queueing that made HTTP/1.1 slow, but it moves a lot of state into the server: stream tables, flow control windows and a compression dictionary per connection. Most HTTP/2 security problems, from the 2019 frame floods to the 2023 Rapid Reset attack (CVE-2023-44487), come from that extra state and from the moment an HTTP/2 request is translated back into HTTP/1.1 behind a proxy. This post walks through the wire format field by field, then the weakness classes as concepts, then the checks you can run against your own servers. Server tuning is covered in more depth in our HTTP/2 hardening guide, so the configuration section here stays short.
Key takeaways
- HTTP/2 is an application layer protocol that runs over TCP, almost always inside TLS. RFC 9113 (June 2022) is the current specification; it obsoletes RFC 7540 (May 2015) and RFC 8740 (February 2020). Header compression is HPACK, RFC 7541 (May 2015).
- Every frame starts with a fixed 9 octet header: a 24 bit length, an 8 bit type, 8 bits of flags, one reserved bit and a 31 bit stream identifier. The default maximum frame payload is 16,384 octets.
- Over TLS, HTTP/2 is selected with the ALPN identifier
h2(RFC 7301). The cleartext Upgrade path (h2c) is deprecated in RFC 9113; cleartext HTTP/2 now works only with prior knowledge. - Flow control is credit based, applies only to DATA frames, and starts at 65,535 octets per stream and per connection.
- RFC 9113 deprecates the RFC 7540 priority tree in favour of the simpler Priority header and PRIORITY_UPDATE frame from RFC 9218 (June 2022).
- The main weakness classes are resource exhaustion through cheap frames (stream resets, control frames, CONTINUATION), header compression oracles, and desynchronisation when an intermediary converts HTTP/2 into HTTP/1.1. CVE-2023-44487 was added to the CISA Known Exploited Vulnerabilities catalog on 10 October 2023.
- You can confirm what your own server negotiates with
curl -w '%{http_version}'andopenssl s_client -alpn h2; patching the HTTP/2 library or server is the fix for most implementation flaws.
What HTTP/2 is for: the layer and the standard
HTTP/1.1 sends each request as lines of text and, on one connection, the responses have to come back in order. Browsers worked around that by opening several TCP connections per host, by inlining images and scripts, and by sharding assets across hostnames. HTTP/2 was built to remove those workarounds without changing what HTTP means. The abstract of RFC 9113 puts it plainly: HTTP/2 enables "a more efficient use of network resources and a reduced latency" by adding field compression and "multiple concurrent exchanges on the same connection".
Where it sits in the stack
HTTP/2 is an application layer protocol. It runs over TCP, and RFC 9113 requires TLS 1.2 or higher when it runs over TLS. It uses the same http and https URI schemes and the same default ports as HTTP/1.1: 80 for cleartext and 443 for TLS. Nothing in a URL tells a client which HTTP version a server speaks, which is why version negotiation happens inside TLS.
The semantics, meaning methods, status codes, header fields and caching rules, are no longer defined inside the HTTP/2 document. Since June 2022 they live in RFC 9110 (HTTP Semantics), which all three HTTP versions share. RFC 9113 only defines how those messages are framed on the wire. That split is why a request can enter a proxy as HTTP/2 and leave it as HTTP/1.1, and why that conversion is a recurring source of bugs.
Why binary frames replaced text
Text framing in HTTP/1.1 depends on finding delimiters: the end of a line, the blank line after headers, and then either a Content-Length value or chunked encoding to find the end of the body. Parsers that disagree about edge cases are the root of HTTP request smuggling. HTTP/2 replaces delimiter parsing with length prefixed frames: the receiver reads 9 octets, learns exactly how long the payload is and which stream it belongs to, and never scans for a line ending. Binary frames also let one connection interleave pieces of many responses.
What RFC 9113 changed from RFC 7540
RFC 7540 defined HTTP/2 in May 2015, alongside HPACK in RFC 7541. RFC 8740 (February 2020) added rules for TLS 1.3. RFC 9113, published in June 2022 as part of the same release as RFC 9110, replaced both. Its Appendix B lists the substantive changes:
- TLS 1.3 rules from RFC 8740 were folded in, including the ban on post-handshake client certificate requests.
- The RFC 7540 priority scheme was deprecated. The frame formats were kept so old peers still parse, but RFC 9113 says the scheme "was not successful" and recommends the RFC 9218 signals instead.
- The HTTP/1.1 Upgrade mechanism to
h2cwas deprecated. It was rarely deployed; cleartext users chose prior knowledge instead. - Validation of field names and values was narrowed, and the checks intermediaries must perform were precisely defined.
- Connection-specific header fields, which are prohibited, were identified more completely.
Hostand:authorityare no longer allowed to disagree.- Rules for HPACK dynamic table size updates after a settings change were clarified.
- The frame type and setting ranges reserved for experiments were opened for general use.
How HTTP/2 works on the wire
Starting a connection
RFC 9113 defines two ways to start HTTP/2. For https URIs, the client offers h2 in the TLS Application-Layer Protocol Negotiation (ALPN) extension, defined in RFC 7301 (July 2014) as TLS extension type 16. The h2 identifier is the two octets 0x68 0x32. If the server selects h2, both sides switch to HTTP/2 framing as soon as the handshake finishes. For http URIs, a client can only use HTTP/2 if it already knows the server supports it, which RFC 9113 calls prior knowledge.
Either way, the client then sends a fixed 24 octet connection preface, the string PRI * HTTP/2.0 followed by line breaks and SM, and then a SETTINGS frame. The preface was chosen so that an HTTP/1.1 server receiving it by mistake stops processing. The server's preface is simply its own SETTINGS frame. Each side acknowledges the other's settings.
Client Server
| TCP SYN / SYN-ACK / ACK |
| TLS ClientHello (ALPN: h2, http/1.1) -------> |
| <------- ServerHello ... (ALPN selected: h2) |
| TLS Finished -------> |
| Preface "PRI * HTTP/2.0 ... SM" + SETTINGS ---> |
| <------- SETTINGS (server preface) |
| SETTINGS (ACK) -------> |
| <------- SETTINGS (ACK) |
| HEADERS stream 1 (GET /index.html, END_STREAM) -> |
| HEADERS stream 3 (GET /app.js, END_STREAM) ----> |
| <------- HEADERS stream 1 (:status 200) |
| <------- DATA stream 3 (part of app.js) |
| <------- DATA stream 1 (part of index.html) |
| WINDOW_UPDATE stream 0 (more credit) -------> |
| <------- DATA stream 1 (END_STREAM) |
| <------- DATA stream 3 (END_STREAM) |
| <------- GOAWAY (last stream 3, NO_ERROR) |
Responses for stream 1 and stream 3 interleave freely; neither waits for the other.
The frame header
All frames share one layout. RFC 9113 section 4.1 draws it like this:
| Field | Size | Meaning |
|---|---|---|
| Length | 24 bits | Payload length in octets, not counting the 9 octet header. Values above 16,384 must not be sent unless the receiver raised SETTINGS_MAX_FRAME_SIZE |
| Type | 8 bits | Frame type. Unknown types must be ignored and discarded |
| Flags | 8 bits | Boolean flags whose meaning depends on the type, for example END_STREAM (0x01), END_HEADERS (0x04), PADDED (0x08) |
| Reserved | 1 bit | Must be sent as zero and ignored on receipt |
| Stream Identifier | 31 bits | Which stream the frame belongs to. Zero means the frame applies to the whole connection |
| Frame Payload | variable | Format depends entirely on the type |
The receiver can advertise a larger maximum frame size, anywhere from 16,384 up to 16,777,215 octets. An oversized frame is a FRAME_SIZE_ERROR, and if the frame could change connection state, such as SETTINGS or any frame carrying headers, the error kills the whole connection.
Frame types
RFC 9113 defines ten frame types, and the IANA HTTP/2 Frame Type registry adds a few more from extensions.
| Code | Type | What it does |
|---|---|---|
| 0x00 | DATA | Carries a request or response body. The only frame type subject to flow control |
| 0x01 | HEADERS | Opens a stream and carries an HPACK encoded header block |
| 0x02 | PRIORITY | Deprecated RFC 7540 priority signal; still parsed, semantics no longer defined in RFC 9113 |
| 0x03 | RST_STREAM | Ends one stream immediately with an error code |
| 0x04 | SETTINGS | Declares connection parameters; must be acknowledged |
| 0x05 | PUSH_PROMISE | Server announces a response it will push without a request |
| 0x06 | PING | Measures round trip time and checks the connection is alive; must be answered |
| 0x07 | GOAWAY | Starts graceful shutdown and names the last stream the sender processed |
| 0x08 | WINDOW_UPDATE | Grants more flow control credit to a stream or the connection |
| 0x09 | CONTINUATION | Carries the rest of a header block that did not fit in one HEADERS or PUSH_PROMISE frame |
| 0x0a | ALTSVC | Advertises an alternative service (RFC 7838) |
| 0x0c | ORIGIN | Lists origins the server is authoritative for (RFC 8336) |
| 0x10 | PRIORITY_UPDATE | Sends a new priority for a stream (RFC 9218) |
A header block that spans several frames must be sent as a contiguous sequence: one HEADERS frame without END_HEADERS, then CONTINUATION frames, and nothing else on the connection until the frame with END_HEADERS arrives. That rule matters later, because the receiver has to buffer the whole block.
Streams and the stream state machine
A stream is one request and its response. Client-initiated streams use odd identifiers (1, 3, 5 and so on) and server-initiated streams, which only exist for push, use even ones. Identifiers only go up and cannot be reused, so a long-lived connection can eventually run out and has to be replaced. Stream 0 is reserved for connection control frames.
Each stream moves through the states drawn in RFC 9113 Figure 2:
| State | How a stream gets here | What can happen next |
|---|---|---|
| idle | Every stream starts here | HEADERS opens it; PUSH_PROMISE reserves it |
| reserved (local / remote) | A PUSH_PROMISE names it | The pushed HEADERS moves it to half-closed; RST_STREAM closes it |
| open | HEADERS sent or received | Either side can send frames; END_STREAM from one side half-closes it |
| half-closed (local) | This side sent END_STREAM | This side only receives; peer END_STREAM or RST_STREAM closes it |
| half-closed (remote) | Peer sent END_STREAM | This side only sends; its END_STREAM or RST_STREAM closes it |
| closed | Both sides ended, or either sent RST_STREAM | Terminal state; late frames are tolerated briefly after a reset |
The concurrency limit counts only streams that are open or half-closed. A server advertises SETTINGS_MAX_CONCURRENT_STREAMS, and RFC 9113 recommends a value no smaller than 100. A client that opens a stream beyond the limit gets a PROTOCOL_ERROR or REFUSED_STREAM. Remember that only those three states count; a stream that has been reset is closed and no longer counts. That accounting rule is the heart of Rapid Reset.
Flow control windows
HTTP/2 multiplexes many streams over one TCP connection, so TCP's own flow control cannot stop one large download from crowding out everything else. HTTP/2 adds its own credit scheme on top. RFC 9113 section 5.2 lists its properties:
- It operates hop by hop, between the two ends of one connection, not end to end through proxies.
- Receivers advertise how many octets they will accept, per stream and for the whole connection, and grant more with WINDOW_UPDATE frames.
- The initial window is 65,535 octets for each new stream and for the connection.
- Only DATA frames consume credit, so control frames are never blocked behind data.
- The maximum window is 2^31 minus 1 octets. A receiver that does not need flow control can advertise the maximum and keep topping it up.
When to send WINDOW_UPDATE and how much to grant is left entirely to the implementation, so two servers can behave very differently under the same abusive client.
SETTINGS and their defaults
| Code | Setting | Initial value | Purpose |
|---|---|---|---|
| 0x01 | SETTINGS_HEADER_TABLE_SIZE | 4,096 octets | Maximum HPACK dynamic table size the sender's decoder will hold |
| 0x02 | SETTINGS_ENABLE_PUSH | 1 | A client sets 0 to refuse server push; a server may only ever send 0 |
| 0x03 | SETTINGS_MAX_CONCURRENT_STREAMS | unlimited | How many streams the peer may have open at once |
| 0x04 | SETTINGS_INITIAL_WINDOW_SIZE | 65,535 octets | Starting per-stream flow control window |
| 0x05 | SETTINGS_MAX_FRAME_SIZE | 16,384 octets | Largest frame payload the sender will accept |
| 0x06 | SETTINGS_MAX_HEADER_LIST_SIZE | unlimited | Advisory limit on uncompressed header size |
| 0x08 | SETTINGS_ENABLE_CONNECT_PROTOCOL | 0 | Allows extended CONNECT, used for WebSockets over HTTP/2 (RFC 8441, September 2018) |
| 0x09 | SETTINGS_NO_RFC7540_PRIORITIES | 0 | Signals that the old priority tree is not used (RFC 9218) |
Two of those initial values are "unlimited". A server that never sends its own limits has, on paper, agreed to unbounded concurrency and unbounded header lists. Real servers ship with limits, but limits a server sends cannot take effect before the client has received them.
Ending streams and connections
RST_STREAM ends one stream with an error code; the rest of the connection carries on. GOAWAY ends the connection gracefully: it carries the highest stream identifier the sender processed, so the peer knows which requests are safe to retry on a new connection. The error codes are registered with IANA:
| Code | Name | Typical meaning |
|---|---|---|
| 0x00 | NO_ERROR | Graceful shutdown |
| 0x01 | PROTOCOL_ERROR | Generic protocol violation |
| 0x02 | INTERNAL_ERROR | Implementation fault |
| 0x03 | FLOW_CONTROL_ERROR | Flow control limits exceeded |
| 0x04 | SETTINGS_TIMEOUT | SETTINGS not acknowledged in time |
| 0x05 | STREAM_CLOSED | Frame arrived for a closed stream |
| 0x06 | FRAME_SIZE_ERROR | Frame too big or too small |
| 0x07 | REFUSED_STREAM | Stream refused before any processing; safe to retry |
| 0x08 | CANCEL | Stream no longer needed |
| 0x09 | COMPRESSION_ERROR | HPACK state could not be maintained |
| 0x0a | CONNECT_ERROR | Tunnel opened by CONNECT failed |
| 0x0b | ENHANCE_YOUR_CALM | Peer is generating excessive load |
| 0x0c | INADEQUATE_SECURITY | TLS parameters do not meet HTTP/2 requirements |
| 0x0d | HTTP_1_1_REQUIRED | Retry this request over HTTP/1.1 |
Message rules that differ from HTTP/1.1
HTTP/2 moves the request line and status line into pseudo-header fields that start with a colon: :method, :scheme, :authority and :path on requests, and :status on responses. Field names must be lowercase. Connection-specific fields are banned outright: Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding and Upgrade make a message malformed, and TE may appear only with the value trailers. Message length comes from the frames, so Content-Length, if present, must match the DATA actually sent. HTTP/2 also has no Upgrade of its own; RFC 9113 says it "purposefully does not support upgrade to another protocol". WebSockets ride on extended CONNECT instead.
Ports and IANA registrations
HTTP/2 has no port of its own: it uses 443 with TLS and 80, or any port agreed out of band, for cleartext with prior knowledge. RFC 7540 registered the h2 and h2c ALPN identifiers and the PRI method, and created the IANA registries for frame types, settings and error codes, which RFC 9113 keeps. RFC 9113 marks the h2c upgrade token and the HTTP2-Settings header as obsolete.
Server push
Server push let a server send a response, announced by PUSH_PROMISE, before the client asked for it, for example a stylesheet alongside the HTML page that references it. RFC 9113 still defines push but is candid that it "is difficult to use effectively" because the server has to guess what the client needs and already has cached. In practice it has faded out. nginx marked its http2_push directives obsolete in version 1.25.1 and points to 103 Early Hints instead. Clients can switch push off with SETTINGS_ENABLE_PUSH set to 0.
How HPACK compresses headers
The problem with SPDY-era compression
Before HTTP/2, the experimental SPDY protocol compressed headers with DEFLATE. RFC 7541 explains that this was "very effective" but exposed a security risk demonstrated by the CRIME attack. DEFLATE removes redundancy by matching prefixes, so an attacker who can inject text into a request and watch the encrypted length can confirm a secret, such as a cookie, one character at a time. TLS hides content, not length. HPACK was designed from scratch to keep most of the compression while closing that per-character channel, and with a strict bound on memory.
Static table, dynamic table and Huffman coding
HPACK replaces each header field with one of a few representations:
- Indexed field. A single small integer that points into a table. The static table, listed in RFC 7541 Appendix A, has 61 fixed entries such as
:method GET,:path /,:status 200and common header names. The dynamic table holds fields seen earlier on this connection, newest first, and evicts the oldest when full. - Literal with incremental indexing. The field is sent in full and added to the dynamic table so the next request can reference it with one byte.
- Literal without indexing. Sent in full and not added to the table for this hop.
- Literal never indexed. Sent in full and marked so that every intermediary that re-encodes the message must also send it as a literal. RFC 7541 suggests this for high value fields such as
CookieandAuthorization.
String values can be Huffman coded with a fixed code table tuned for HTTP header text. The code is static, so it does not adapt to the content, and RFC 7541 records no known practical attack against it.
Size accounting and bounded memory
Each dynamic table entry counts as the length of its name plus the length of its value plus 32 octets of overhead. The decoder sets the ceiling with SETTINGS_HEADER_TABLE_SIZE, 4,096 octets by default, and the encoder may use less but never more. That makes HPACK's state predictable, which DEFLATE's window was not. SETTINGS_MAX_HEADER_LIST_SIZE uses the same 32 octet per field accounting for the uncompressed size of a header list.
What HPACK fixed, and what it did not
RFC 7541 section 7.1.1 says HPACK "mitigates but does not completely prevent" CRIME style attacks. An attacker can no longer confirm one character at a time; a guess has to match an entire field value to compress better. High entropy secrets, like a long random session token, become impractical to recover. Short or predictable values remain at risk when two parties who do not trust each other share one compression context, for example when a proxy sends requests from many users over a single upstream connection. The standard's mitigations are to keep separate contexts per trust domain, to stop indexing a field after too many different values, and to mark sensitive fields as never indexed.
Prioritisation: from RFC 7540 trees to RFC 9218
RFC 7540 let clients build a dependency tree of streams with weights, so a browser could say "send the CSS before the images". Implementations disagreed on how to honour it, and servers often ignored it. RFC 9113 deprecated the scheme. Its replacement, RFC 9218 (June 2022), is version independent and much simpler:
- A
Priorityrequest header carries two parameters. Urgencyuruns from 0 (most urgent) to 7, with a default of 3; level 7 is reserved for background work. Incrementalisays whether the response is useful in pieces, defaulting to false. - A PRIORITY_UPDATE frame, type 0x10, lets a client change a stream's priority after the request is sent.
- The SETTINGS_NO_RFC7540_PRIORITIES setting, 0x09, tells the peer the old tree is not in use.
Priority is a hint; a server is never obliged to follow it.
Versions and history
| Date | Milestone | What changed |
|---|---|---|
| July 2014 | RFC 7301 (ALPN) | The TLS extension HTTP/2 uses to negotiate h2, published the year before HTTP/2 |
| May 2015 | RFC 7540 (HTTP/2) and RFC 7541 (HPACK) | Binary framing, streams, flow control, server push, priority tree and HPACK published on the Standards Track |
| September 2018 | RFC 8441 | Extended CONNECT, so WebSockets can run over an HTTP/2 stream |
| 13 August 2019 | Third party HTTP/2 denial of service advisory cited in RFC 9113 | Several implementations found vulnerable to floods of cheap frames |
| February 2020 | RFC 8740 | Rules for TLS 1.3 with HTTP/2, including no post-handshake client authentication |
| June 2022 | RFC 9110, RFC 9113, RFC 9114, RFC 9218 | Shared HTTP semantics; HTTP/2 revised; HTTP/3 published; new priority scheme |
| 10 October 2023 | CVE-2023-44487 published and added to the CISA KEV catalog | Rapid Reset, exploited in the wild from August to October 2023 according to the CVE record and CISA |
| 4 April 2024 | Apache HTTP Server 2.4.59 | Fix for CVE-2024-27316, memory exhaustion from endless CONTINUATION frames |
What is deployed today is RFC 9113 HTTP/2 over TLS 1.2 or 1.3, negotiated with ALPN. Push is rarely used, the old priority tree is ignored by most servers, and cleartext h2c survives mainly on internal links, for example between a load balancer and gRPC services, where it is configured by prior knowledge. Apache httpd's documentation notes that h2c "has been removed from the current specification but httpd still supports it".
Security properties of HTTP/2
RFC 9113 opens its Security Considerations by saying many of its claims "do not hold unless TLS is used". Taking the usual properties one at a time:
| Property | What HTTP/2 itself provides | Where it actually comes from |
|---|---|---|
| Confidentiality | None in the framing layer | TLS, which RFC 9113 requires to be 1.2 or higher with restrictions |
| Integrity | None beyond length framing | TLS record protection |
| Server authentication | None | The TLS certificate checked against :authority |
| Client authentication | None | Application credentials, or a client certificate during the TLS handshake (never after it, under TLS 1.3) |
| Replay protection | None at the HTTP layer | TLS; early data (0-RTT) can be replayed and needs the RFC 8470 rules |
| Availability | Settings bound some state; most limits are left to implementations | Server limits, patches and capacity |
| Protocol confirmation | The h2 ALPN choice and the preface | ALPN is authenticated by the TLS handshake |
For HTTP/2 over TLS 1.2, RFC 9113 requires TLS compression and renegotiation to be disabled, discourages the weak cipher suites listed in its Appendix A, and requires support for TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 on P-256 so compliant peers always share a suite. Under TLS 1.3, servers must not send post-handshake certificate requests, and clients must send Server Name Indication. Our posts on the TLS 1.3 handshake, what to disable in TLS 1.2 and 0-RTT replay risk cover the TLS side in depth.
One subtle point about downgrade: an on-path attacker cannot silently strip h2 from the ALPN list, because the TLS handshake transcript is authenticated. What can cause a connection to fall back to HTTP/1.1 is a legitimate party in the middle, such as a corporate proxy or a load balancer that terminates TLS and opens its own connection to the origin.
Weakness classes, explained as concepts
RFC 9113 section 10.5 says plainly that an HTTP/2 connection "can demand a greater commitment of resources to operate than an HTTP/1.1 connection". Most of the classes below follow from that. None of them is a reason to avoid HTTP/2; each is a reason to keep the implementation patched and its limits set.
Stream and reset floods (Rapid Reset, CVE-2023-44487)
The CVE record describes the issue in one sentence: the HTTP/2 protocol "allows a denial of service (server resource consumption) because request cancellation can reset many streams quickly". The concept follows from the state machine above. A client opens a stream with HEADERS, which costs the client almost nothing, and the server starts work on the request, perhaps routing it to a backend. The client then cancels the stream with RST_STREAM, which is a legitimate action that browsers take all the time. The stream moves to closed, so it no longer counts against SETTINGS_MAX_CONCURRENT_STREAMS, and the client is free to open another. The concurrency limit was meant to cap how much work one connection can create, but it only counts streams that are still open, not work the server has already committed to.
Because it follows from how the protocol counts streams, one protocol level CVE covered many independent implementations. CISA's alert of 10 October 2023 says it was exploited in the wild from August through October 2023 and recommends patching and configuration changes. CISA's enrichment in the CVE record scores it CVSS 3.1 7.5 (availability impact high) and lists it in the KEV catalog with a date added of 10 October 2023. The general remedy follows RFC 9113 section 10.5: implementations track how each connection uses features such as stream cancellation, and may treat excessive use as a connection error of type ENHANCE_YOUR_CALM. Apache HTTP Server 2.4.58, released 19 October 2023, also fixed a related bug (CVE-2023-45802) found while testing for Rapid Reset, where memory for a reset stream was not reclaimed until the connection closed. nginx 1.25.3, released 24 October 2023, lists "improved detection of misbehaving clients when using HTTP/2".
Control frame floods
The 2019 advisory that RFC 9113 cites showed that several frames force the receiver to do work or reply: every PING must be answered, every SETTINGS must be acknowledged, an invalid request can trigger an RST_STREAM in reply, and tiny WINDOW_UPDATE increments make a sender cut its output into many small DATA frames. RFC 9113 lists these alongside floods of empty frames, pointless SETTINGS changes and PRIORITY churn. The design allows it because each of these features has a legitimate use, so the protocol cannot forbid them; it can only tell implementations, in RFC 9113's words, to "track the use of these features and set limits on their use".
Header list size exhaustion and CONTINUATION floods
A header block may be split across one HEADERS frame and any number of CONTINUATION frames, and nothing else can be sent on the connection until the block ends. RFC 9113 section 10.5.1 notes that fields critical for routing can appear at the end, so a server may need to buffer the whole block, and "there is no hard limit to the size of a field block". SETTINGS_MAX_HEADER_LIST_SIZE is only advisory, and the receiver must still process an oversized block to keep its HPACK state consistent unless it closes the connection. If an implementation keeps accepting CONTINUATION frames, or keeps decompressing into memory while waiting to send a polite 431 error, a peer can exhaust memory or CPU. That is what Apache's CVE-2024-27316 described: incoming headers over the limit were buffered "in order to generate an informative HTTP 413 response", and a client that never stopped sending headers caused memory exhaustion.
Compression side channels
HPACK narrows the CRIME channel to whole-value guesses but does not remove it, and RFC 9113 section 10.6 extends the warning to compressed response bodies: an implementation "MUST NOT compress content that includes both confidential and attacker-controlled data" unless the two use separate compression dictionaries. The design allows the leak because compression and encryption together turn the length of the ciphertext into a signal. Padding, which HTTP/2 offers on DATA and HEADERS frames, helps only a little; RFC 9113 says that at best it increases the number of observations an attacker needs, and recommends limiting compression over relying on padding.
Downgrade and translation to HTTP/1.1 at proxies
Many deployments terminate HTTP/2 at an edge proxy or load balancer and speak HTTP/1.1 to the application behind it. That conversion is the most important trust boundary in an HTTP/2 deployment. HPACK can carry characters in names and values, such as line breaks, that would be delimiters in HTTP/1.1. If a proxy copies such a value into an HTTP/1.1 request without validating it, the backend may see two requests where the proxy saw one. The same desynchronisation happens when a proxy forwards a Content-Length that disagrees with the actual body, or passes through a banned connection-specific header such as Transfer-Encoding. RFC 9113 section 10.3 makes validation mandatory for intermediaries that translate messages, and warns that building a request line by concatenating :method, :scheme, :authority and :path "is not secure unless the input values are fully validated".
Cleartext h2c and upgrade tunnels
Cleartext HTTP/2 has, in RFC 9113's words, "minimal protection against cross-protocol attacks". The preface is designed to confuse HTTP/1.1 servers but offers nothing for other protocols. The deprecated Upgrade path adds a second concern. If a front proxy relays an HTTP/1.1 request carrying Upgrade: h2c to a backend that accepts it, the connection can turn into a long-lived HTTP/2 session that the proxy treats as an opaque tunnel, so per-request rules the proxy applies, such as path based access control, no longer see the requests inside. The practical lesson is to make sure nothing on a public path honours it.
Push, fingerprinting and timing
Section 10.4 warns that on a multi-tenant server one tenant must not be able to push a response for a resource it does not control, or a cache could serve the wrong version. Section 10.8 notes that SETTINGS values, window management and priorities can fingerprint a client, and section 10.9 that packing many requests into one packet makes remote timing attacks more precise. The defence against timing is in the application: processing time should not depend on secret values.
Secure configuration, with defender side check commands
This section is deliberately brief; the HTTP/2 hardening guide goes deeper into per-server limits for Rapid Reset, CONTINUATION and the 2019 floods. Only run these commands against systems you own or are authorised to test.
1. Confirm what your endpoint negotiates
The curl manual documents --http2 (negotiate HTTP/2 in the TLS handshake, which curl does by default for HTTPS), --http2-prior-knowledge (speak HTTP/2 directly without Upgrade) and the http_version write-out variable, "the http version that was effectively used".
# Which HTTP version did the server actually use? Prints 2 for HTTP/2.
curl -sS -o /dev/null --http2 -w '%{http_version}\n' https://your-host.example/
# Is cleartext HTTP/2 (h2c) accepted on an internal port? It should only be where you intend it.
curl -sS -o /dev/null --http2-prior-knowledge -w '%{http_version}\n' http://internal-service.example:8080/
The curl manual also notes that curl "does not itself insist on TLS 1.2 or higher" for HTTP/2 and suggests adding --tlsv1.2 when you want to enforce it in a test.
The OpenSSL s_client manual documents -alpn for offering a comma separated protocol list, -servername for SNI and per-version switches such as -tls1_1:
# Offer h2 and http/1.1; the session summary shows which ALPN protocol the server selected.
openssl s_client -connect your-host.example:443 -servername your-host.example -alpn h2,http/1.1 </dev/null 2>/dev/null | grep -i alpn
# A server that follows RFC 9113 should refuse HTTP/2 below TLS 1.2. Expect this handshake to fail.
openssl s_client -connect your-host.example:443 -servername your-host.example -alpn h2 -tls1_1 </dev/null
2. nginx
The nginx ngx_http_v2_module documentation lists http2 on | off (default off, added in 1.25.1) and http2_max_concurrent_streams with a default of 128. Header size limits for HTTP/2 now come from the core large_client_header_buffers directive (default 4 8k), whose documentation says the limits "are similarly applied in HTTP/2 and HTTP/3 connections". keepalive_requests (default 1000) caps the requests served over one connection. The older http2_max_field_size, http2_max_header_size and http2_max_requests directives are obsolete since 1.19.7, and the http2_push directives since 1.25.1. The -T switch tests the configuration and dumps it.
nginx -v
nginx -T 2>/dev/null | grep -nE 'http2|large_client_header_buffers|keepalive_requests|keepalive_timeout'
nginx 1.29.4 (9 December 2025) added HTTP/2 support to ngx_http_proxy_module for upstreams; if you still convert to HTTP/1.1 upstream, that proxy is the translation boundary described above.
3. Apache HTTP Server
The mod_http2 documentation lists H2MaxSessionStreams (default 100), H2ModernTLSOnly (default on, requiring TLS 1.2 or newer and none of the prohibited cipher suites), H2Push (default on) and H2Upgrade, which defaults to on for h2c and off for h2. The Protocols directive decides which protocols a virtual host offers; the HTTP/2 how-to shows Protocols h2 http/1.1 for TLS hosts. The httpd manual documents -M to list loaded modules and -S to show parsed virtual host settings.
httpd -M 2>/dev/null | grep -i http2
httpd -S
grep -RniE '^\s*(Protocols|H2MaxSessionStreams|H2ModernTLSOnly|H2Push|H2Upgrade)\b' /etc/httpd /etc/apache2 2>/dev/null
Remove h2c from public virtual hosts unless something needs it, turn H2Push off unless you rely on push, keep H2ModernTLSOnly on, and stay at or after 2.4.59, which carries the CONTINUATION fix.
4. Envoy
Envoy's edge proxy best practices say its defaults are "tailored for the service mesh use case" and recommend, for edge use, limiting HTTP/2 concurrent streams to 100, the initial stream window to 64 KiB and the initial connection window to 1 MiB, alongside an overload manager, connection limits and timeouts. You can read the running values from the admin interface, which Envoy's own documentation says should only be reachable on a secure network, ideally localhost:
curl -s http://127.0.0.1:ADMIN_PORT/config_dump | grep -E 'max_concurrent_streams|initial_stream_window_size|initial_connection_window_size'
5. Find the library, not just the server
Many HTTP/2 implementations are libraries linked into application servers, language runtimes, gRPC stacks and proxies. A server that looks patched may sit in front of an application whose own HTTP/2 stack is not. The Node.js July 2026 security release is a recent example of HTTP/2 fixes landing in a language runtime rather than a web server. Inventory where HTTP/2 code actually runs before you decide you are done.
Where Safeguard fits
Safeguard does not inspect, filter or rate limit HTTP/2 traffic, and none of the configuration above depends on it. The useful connection is the code. Safeguard's software composition analysis, generally available, builds a full transitive dependency inventory of repositories and container images, so it shows which projects bundle an HTTP/2 library or embed a runtime with an HTTP/2 stack, and at which versions. SBOM generation and analysis in SPDX and CycloneDX is generally available, as is vulnerability management with EPSS and CISA KEV enrichment, which matters for a KEV-listed issue like CVE-2023-44487, and reachability analysis to rank findings. DAST, also generally available, runs first-party dynamic tests against targets you have verified you own over HTTP, WebSocket and APIs; it is not a load or flood testing tool. Our post on using SBOMs to find affected products fast shows the lookup flow.
HTTP/2 vs HTTP/3 vs HTTP/1.1: when to use which
HTTP/3 (RFC 9114, June 2022) carries the same semantics over QUIC on UDP. QUIC gives each stream independent loss recovery, so a lost packet stalls only the stream it belonged to, whereas HTTP/2's streams all share one TCP byte stream and a single loss holds up every stream behind it. HTTP/3 uses QPACK instead of HPACK; our post on QPACK amplification covers one of its own resource risks, and our TCP vs UDP explainer covers what each transport leaves visible.
| Use case | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Public website or API for browsers | Fallback only | Default over TLS; widely supported | Add alongside HTTP/2 for lossy mobile networks; clients usually discover it via Alt-Svc |
| Internal service to service, gRPC | Works, but no multiplexing | Natural fit; gRPC is built on HTTP/2 | Possible, but less tooling and UDP may be filtered internally |
| Through corporate proxies and middleboxes | Most compatible | Usually fine when TLS terminates at a known proxy | UDP 443 is often blocked, so clients fall back |
| Security properties | Text framing, smuggling risk at parser edges | Length-delimited frames; TLS required in practice; per-connection state to bound | Encryption built into QUIC, including most transport headers |
| Main availability risk | Slow request and connection exhaustion | Stream, reset and control frame floods; header block size | QPACK and QUIC connection state; UDP amplification limits in QUIC |
| Operational cost | Lowest; easy to debug | Moderate; needs ALPN, tuned limits and patched libraries | Highest; UDP firewalling, newer libraries, different observability |
A short rule: serve HTTP/2 over TLS everywhere a browser or modern client connects, keep HTTP/1.1 enabled as a fallback, add HTTP/3 where users are on mobile or lossy networks and you can operate UDP, and avoid cleartext h2c except on internal links you control end to end.
Common mistakes
- Treating HTTP/2 as encryption. HTTP/2 has no confidentiality of its own.
h2cover port 80 is plaintext, and the protection inh2comes entirely from TLS. - Assuming the concurrent stream limit caps load. It counts only open and half-closed streams. Reset streams fall out of the count, which is what Rapid Reset relied on. Limits need to cover reset rates and requests per connection too.
- Leaving protocol defaults in place. SETTINGS_MAX_CONCURRENT_STREAMS and SETTINGS_MAX_HEADER_LIST_SIZE are unlimited by default in the protocol. Servers set their own values, but custom or embedded implementations sometimes do not.
- Patching the edge and forgetting the app. HTTP/2 stacks live in language runtimes, gRPC libraries and sidecar proxies as well as web servers. A patched load balancer does not fix a vulnerable runtime behind it if that runtime also terminates HTTP/2.
- Translating to HTTP/1.1 without validation. A proxy that converts HTTP/2 to HTTP/1.1 must validate field values and
Content-Lengthand remove connection-specific headers. Skipping that reopens request smuggling. - Leaving h2c upgrade enabled on public hosts. Apache's
H2Upgradedefaults to on forh2c. Ifh2cis listed inProtocolson a public virtual host behind a proxy, review it. - Using server push as a performance fix. Push is hard to get right, is deprecated in RFC 9113 according to the Apache HTTP/2 guide, is obsolete in nginx since 1.25.1, and can push the wrong thing to a cache in a multi-tenant setup. nginx points to 103 Early Hints instead.
FAQ
What port does HTTP/2 use?
The same ports as HTTP/1.1: 443 for HTTP/2 over TLS (h2) and 80 for cleartext HTTP/2 (h2c) with prior knowledge. HTTP/2 has no dedicated port; the version is chosen during the TLS handshake with ALPN.
Is HTTP/2 encrypted?
Not by itself. The HTTP/2 specification allows cleartext operation, but the Apache HTTP/2 guide notes that most browsers speak HTTP/2 only over https, and RFC 9113 requires TLS 1.2 or higher with some cipher suites and features disabled. The encryption comes from TLS, not from HTTP/2 framing.
What is the difference between h2 and h2c?
h2 is HTTP/2 over TLS, negotiated with ALPN. h2c is HTTP/2 over cleartext TCP. RFC 9113 deprecated the HTTP/1.1 Upgrade route to h2c, so cleartext HTTP/2 is now started by prior knowledge, typically on internal links.
What changed between RFC 7540 and RFC 9113?
RFC 9113 (June 2022) folded in the TLS 1.3 rules from RFC 8740, deprecated the priority tree and the h2c Upgrade mechanism, tightened field validation, banned disagreement between Host and :authority, and moved HTTP semantics into RFC 9110. The wire format stayed compatible.
How does HPACK work?
HPACK replaces header fields with indexes into a 61 entry static table and a per-connection dynamic table of recently sent fields, and can Huffman code literal strings. The decoder caps the dynamic table size (4,096 octets by default). Sensitive fields can be marked never indexed so no hop compresses them.
What was the HTTP/2 Rapid Reset attack?
CVE-2023-44487 is a denial of service class in which a client opens streams and cancels them immediately. Cancelled streams stop counting toward the server's concurrency limit while the server may already have started work, so one connection can generate far more work than the limit suggests. CISA reported exploitation from August to October 2023. The fix is to patch your HTTP/2 server and libraries and apply vendor recommended limits.
How do I check if a website supports HTTP/2?
On a site you own or are authorised to test, run curl -sS -o /dev/null -w '%{http_version}\n' https://your-host.example/; it prints 2 for HTTP/2. openssl s_client -alpn h2,http/1.1 also shows which ALPN protocol the server selected.
HTTP/2 vs HTTP/3: which should I use?
Use HTTP/2 as the baseline for anything that speaks TLS over TCP, including gRPC. Add HTTP/3 for public services where clients are on mobile or lossy networks and you can run UDP 443, because QUIC avoids head of line blocking across streams. Keep HTTP/2 as the fallback, since many networks still block UDP.
Fix it without waiting for upstream
Most HTTP/2 vulnerabilities, Rapid Reset included, were fixed in code rather than in the protocol: in web servers, proxies, language runtimes and the HTTP/2 libraries your applications import. Those libraries are dependencies like any other, which means the gap between a fix shipping and your builds picking it up is yours to close.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show dependency, SBOM and remediation workflows, not traffic inspection. You can download Safeguard and follow the CLI docs to try them on your own projects.
This demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. Pointed at the container image for a gRPC service or reverse proxy, the same scan lists the HTTP/2, HPACK and TLS libraries inside it with their versions and any known vulnerabilities, so you can see which images still carry a stack that predates the Rapid Reset fixes.
Click through it at your own pace, or open it full screen.
This demo is a real CLI session: install, open the interactive terminal, ask it to fix the lodash CVE in a repository, and watch it read the finding and advisory, ask before each command, move lodash to 4.17.21, run npm install and npm test with approval, and print a summary without pushing anything. The same approve each step flow applies when the dependency to bump is an HTTP/2 library or a runtime pinned in your manifest or base image.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Once connected, you can ask in plain language which of your projects include a given HTTP/2 library version, and get the answer from your own scan data rather than from memory.
Click through it at your own pace, or open it full screen.
Quick check
Your server advertises SETTINGS_MAX_CONCURRENT_STREAMS of 100. Why does that not cap how many requests one connection can start?
The limit counts only streams that are open or half-closed. A client that opens a stream and immediately resets it moves that stream to closed, so it stops counting, and the client can open another at once. The server may already have started work for each reset stream. That gap is the idea behind Rapid Reset, CVE-2023-44487, and why limits need to cover how fast a connection cancels streams, not only how many are open.
HPACK replaced DEFLATE header compression. Does that make cookies safe from compression attacks?
Mostly, but not completely. HPACK forces a guess to match a whole field value, so long random tokens are impractical to recover, but short or low entropy values can still be confirmed when untrusted parties share one compression context, for example through a proxy. Marking sensitive fields as never indexed and keeping separate contexts per trust domain close the remaining gap.
Sources
- RFC 9113: HTTP/2, June 2022
- RFC 7541: HPACK, Header Compression for HTTP/2, May 2015
- RFC 9218: Extensible Prioritization Scheme for HTTP, June 2022
- RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2), May 2015 (obsoleted)
- RFC 8740: Using TLS 1.3 with HTTP/2, February 2020 (obsoleted)
- RFC 7301: TLS Application-Layer Protocol Negotiation Extension, July 2014
- RFC 8441: Bootstrapping WebSockets with HTTP/2, September 2018
- RFC 9110: HTTP Semantics, June 2022
- RFC 9114: HTTP/3, June 2022
- IANA: Hypertext Transfer Protocol version 2 (HTTP/2) Parameters
- CVE record: CVE-2023-44487
- CISA alert: HTTP/2 Rapid Reset Vulnerability, CVE-2023-44487, 10 October 2023
- Apache HTTP Server 2.4 vulnerabilities (CVE-2023-45802, CVE-2024-27316)
- Apache HTTP Server: mod_http2 directives
- Apache HTTP Server: HTTP/2 guide
- Apache HTTP Server: httpd command line options
- nginx: ngx_http_v2_module
- nginx: ngx_http_core_module
- nginx: command-line parameters
- nginx: CHANGES
- Envoy: configuring Envoy as an edge proxy
- Envoy: administration interface
- curl man page
- OpenSSL 3.0: openssl-s_client
Last reviewed: 23 October 2026. Server defaults and directive names change between releases; check your own version's documentation before applying the settings above.