๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Comparisons

HTTP/2 vs HTTP/1.1 for Web Scraping

HTTP/2 vs HTTP/1.1 for web scraping: why the deciding factor is not speed but the h2 fingerprint your SETTINGS frames and pseudo-header order leak.

S SparkProxy 1 18 min read
Share
HTTP/2 vs HTTP/1.1 for Web Scraping

HTTP/2 vs HTTP/1.1 for web scraping is decided by fingerprinting, not throughput: an h2 connection makes your client announce itself through SETTINGS values, a flow-control window, priority signalling, and a pseudo-header order that anti-bot vendors hash and compare against the browser your TLS handshake claims to be.

The speed argument gets all the attention and matters least. On a real scraping workload the bottleneck is target latency, proxy quality, and your parse step, not frame encoding. What HTTP/2 genuinely changes is how loud your client is. Every h2 connection opens with a handshake full of implementation-specific numbers, and those numbers are stable enough per library that one short string separates Chrome from Go's net/http from Python's h2. This guide covers what actually changed at the protocol level, how the Akamai-style h2 fingerprint gets assembled, which clients speak h2 at all, and how your proxy choice affects what the target sees.

HTTP/2 vs HTTP/1.1 at a glance

DimensionHTTP/1.1HTTP/2
Specification[RFC 9112](https://www.rfc-editor.org/rfc/rfc9112.html) (was RFC 7230)[RFC 9113](https://www.rfc-editor.org/rfc/rfc9113.html) (obsoletes RFC 7540)
Wire formatText, one request per connection at a timeBinary frames, streams multiplexed
Connections per hostPool of about six in browsersUsually one
Header encodingPlain text, repeated in full[HPACK](https://www.rfc-editor.org/rfc/rfc7541.html) with static and dynamic tables
Header caseMixed case allowedField names must be lowercase
Ordering signalHeader orderHeader order plus pseudo-header order
Handshake chatterNone beyond TLSPreface, SETTINGS, WINDOW_UPDATE, optional PRIORITY
Passive fingerprint surfaceSmallLarge and highly discriminative
Head-of-line blockingAt the request layerMoved down to TCP
Negotiated byDefault assumptionTLS ALPN token `h2`

The last three rows are the whole article. Everything else is performance trivia that a scraper rarely feels.

What HTTP/2 actually changed

HTTP/2 keeps HTTP semantics and replaces the transport syntax. Requests and responses become sequences of binary frames carried on numbered streams, and many streams share one TCP connection. RFC 9113 defines the frame types you care about: HEADERS, DATA, SETTINGS, WINDOW_UPDATE, PRIORITY, RST_STREAM, GOAWAY and PING.

Three practical consequences for scraping:

One connection instead of a pool

HTTP/1.1 clients open several sockets per host because each socket handles one in-flight request. HTTP/2 sends dozens of concurrent requests down a single socket. Fewer TCP and TLS handshakes per page, which matters most on high-latency residential exits.

A mandatory opening ritual

Before any request, an h2 client sends the connection preface (the literal bytes PRI * HTTP/2.0 followed by SM and blank lines) and then a SETTINGS frame, per RFC 9113 section 3.4. That SETTINGS frame is where the fingerprint starts.

Pseudo-headers replace the request line

Method, scheme, authority and path become the pseudo-header fields :method, :scheme, :authority and :path, which must precede all regular fields (RFC 9113 section 8.3.1). The spec constrains their position relative to normal headers but not their order relative to each other, so every implementation picked its own and never changed it.

You negotiate all of this in TLS, not in HTTP. The ALPN extension in the ClientHello offers a token list, and the presence of h2 in that list is what switches the connection to HTTP/2:

curl --http2 -sv https://www.sparkproxy.io/ -o /dev/null 2>&1 | grep -iE "alpn|using http"
# * ALPN: curl offers h2,http/1.1
# * ALPN: server accepted h2
# * using HTTP/2

Server push existed in RFC 7540 and is effectively dead. Chrome removed support in 2022, and modern clients advertise SETTINGS_ENABLE_PUSH = 0. That zero is itself a fingerprint bit.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Head-of-line blocking moved, it did not disappear

HTTP/1.1 blocks at the request layer. A connection carries one exchange at a time, pipelining never worked in practice, and browsers compensate by opening roughly six connections per origin. Slow response, blocked connection.

HTTP/2 removes that: a stalled stream does not stall its siblings. What it cannot remove is TCP's own ordering guarantee. All streams ride one byte sequence, so a single lost segment stalls delivery for every stream on that connection until retransmission completes. On a clean datacenter link you never notice. On a mobile or residential exit with two to four percent loss, one dropped packet can pause forty concurrent scrapes at once, where six independent HTTP/1.1 sockets would have lost only the requests on the affected socket.

That is the one case where HTTP/1.1 genuinely wins on speed, and it is worth measuring rather than assuming. RFC 9114 solves the problem properly by moving HTTP/3 onto QUIC, where every stream gets independent loss recovery, but HTTP/3 through most proxy stacks is still awkward because UDP relay support is uneven.

If you are already tuning concurrency, treat h2 stream limits as a separate budget from socket limits. One h2 connection with SETTINGS_MAX_CONCURRENT_STREAMS = 100 is not the same shape of load as 100 HTTP/1.1 sockets, and your async scraping architecture needs to know which one it is running.

HPACK: compression that also leaks state

HTTP/2 compresses headers with HPACK, defined in RFC 7541. It has three moving parts: a fixed static table of 61 common field entries, a per-connection dynamic table that both peers keep in sync, and optional Huffman coding of literal values.

The compression is real. A second request on the same connection can shrink a 700-byte header block to a few dozen bytes, because most fields are already indexed. The catch for scrapers is that all of that state is per connection.

Here is the part most guides miss. Rotating your exit IP on every request forces a brand new TCP connection, a new TLS handshake and a new h2 preface every time. You throw away the dynamic table and pay full header cost, and more importantly you hand the target a fresh, complete copy of your h2 fingerprint on every single request. Per-request rotation does not hide you at the HTTP layer. It maximises the number of independent observations the defender collects, which is exactly what they need to cluster hundreds of unrelated IPs into one bot identity. Sticky sessions that hold an exit for a batch of requests leak less, not more.

HPACK choices are themselves fingerprintable: whether the client Huffman-encodes literals, whether it inserts fields into the dynamic table or sends them as literals without indexing, and what SETTINGS_HEADER_TABLE_SIZE it advertises. Chrome, Firefox and the Python h2 library each make different calls.

The h2 fingerprint: what anti-bot systems hash

The canonical work here is Akamai's research paper, Passive Fingerprinting of HTTP/2 Clients by Elad Shuster and Ory Segal, presented at Black Hat EU 2017. It defines a compact string built from four fields of the connection opening, joined by pipes:

<SETTINGS>|<WINDOW_UPDATE>|<PRIORITY>|<PSEUDO_HEADER_ORDER>
  • SETTINGS: each advertised parameter as id:value, joined by ;, in the order sent.
  • WINDOW_UPDATE: the connection-level flow control increment, or 00 if the client sends none.
  • PRIORITY: each PRIORITY frame as streamId:exclusivity:dependentStreamId:weight, joined by ,, or 0 if there are none.
  • PSEUDO_HEADER_ORDER: the first letters of the pseudo-headers in the order emitted, such as m,a,s,p.

Commonly observed values (these drift with browser releases, so treat them as shapes rather than constants):

Chrome    1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p

Firefox   1:65536;4:131072;5:16384|12517377|3:0:0:201,5:0:0:101,7:0:0:1,
          9:0:7:1,11:0:3:1,13:0:0:241|m,p,a,s

Firefox is instantly recognisable because it opens with a set of PRIORITY frames that build a dependency tree for resource classes. Chrome sends none. Neither behaviour is required by the spec, which is precisely why it identifies them.

The SETTINGS identifiers come from RFC 9113 section 6.5.2:

IDNameSpec defaultWhat varies in practice
0x01HEADER_TABLE_SIZE4096Browsers raise it to 65536; many libraries omit it
0x02ENABLE_PUSH1Modern clients send 0; some libraries omit it
0x03MAX_CONCURRENT_STREAMSunlimitedChrome sends 1000; most libraries omit it
0x04INITIAL_WINDOW_SIZE65535Chrome 6291456, Firefox 131072, Go 4194304
0x05MAX_FRAME_SIZE16384Firefox and Go send it, Chrome does not
0x06MAX_HEADER_LIST_SIZEunlimitedChrome 262144, Go configurable

Two clients can advertise identical values and still hash differently, because the fingerprint preserves the order the parameters were written in. That is the trap for anyone hand-rolling an h2 client: matching Chrome's numbers is not enough, you have to match Chrome's sequence.

One more wrinkle worth knowing. RFC 9113 marks the original stream-priority scheme from RFC 7540 as deprecated, and RFC 9218 defines a replacement that signals urgency through a Priority header field instead of PRIORITY frames. Clients migrated at different times and in different ways, so priority behaviour has become an even sharper discriminator than it was in 2017.

Why the mismatch gets you blocked

A defender sees three layers on one connection, and they must agree.

  1. TLS ClientHello: cipher suites, extensions, curves, ALPN list. This is the JA3 or JA4 layer, covered in our guide to TLS fingerprinting.
  2. HTTP/2 opening: SETTINGS, WINDOW_UPDATE, PRIORITY, pseudo-header order.
  3. Request content: User-Agent, header set, header order, Client Hints.

Claiming Chrome in layers one and three while emitting a library fingerprint in layer two is a hard contradiction. No real Chrome build has ever produced that combination, so classifying it needs no machine learning. A lookup table does it.

Go's standard library is the clearest example, and you can read the behaviour straight from the source. In golang.org/x/net/http2/transport.go the initial settings list is assembled as ENABLE_PUSH first, then INITIAL_WINDOW_SIZE (transportDefaultStreamFlow, 4 MiB), then MAX_FRAME_SIZE, then optionally MAX_HEADER_LIST_SIZE and HEADER_TABLE_SIZE. It follows with a connection-level WINDOW_UPDATE of transportDefaultConnFlow, 1 GiB, and sends no PRIORITY frames at all. Chrome writes HEADER_TABLE_SIZE first. Go writes it last, or not at all.

The pseudo-header order is even more damning. Go's request encoder emits them like this:

// golang.org/x/net/internal/httpcommon/request.go
f(":authority", host)
f(":method", m)
f(":path", path)
f(":scheme", req.URL.Scheme)
// fingerprint field: a,m,p,s

Chrome emits m,a,s,p. Firefox emits m,p,a,s. Four pseudo-headers, one ordering choice, and a Go binary wearing a Chrome User-Agent is identified before it has sent a byte of request body.

Which clients actually speak HTTP/2

ClientHTTP/2Browser-matched h2 fingerprintNotes
Python `requests` / urllib3NoNoHTTP/1.1 only; no h2 in urllib3 2.x
Python `aiohttp`NoNoHTTP/1.1 only
Python `httpx`OptionalNo`pip install httpx[http2]`, then `http2=True`
Python `curl_cffi`YesYesImpersonates browser TLS and h2 together
`curl` (system build)YesNoGenuine h2, but a curl fingerprint
`curl-impersonate`YesYesPatched build matching browser handshakes
Go `net/http`YesNoDistinctive Go fingerprint, see above
Node `undici`OptionalNo`allowH2: true` on the Agent
Node `http2` moduleYesNoLow level, server oriented
ScrapyExperimentalNoOpt-in `H2DownloadHandler`
Playwright with ChromiumYesYesIt is the browser

Turning h2 on in httpx is one flag, and it works through a proxy:

import httpx

client = httpx.Client(
    http2=True,
    proxy="http://user:pass@proxy.sparkproxy.io:8000",
    timeout=30.0,
)
r = client.get("https://www.sparkproxy.io/")
print(r.http_version)  # "HTTP/2"

That gets you HTTP/2. It does not get you a browser fingerprint, because httpx delegates framing to the h2 library, which has its own SETTINGS shape. The same distinction runs through the wider requests vs httpx comparison: async and h2 are ergonomics wins, not stealth wins.

For stealth, the impersonating clients are the only shortcut that holds up, because they patch the TLS stack and the h2 layer in one place:

from curl_cffi import requests

r = requests.get(
    "https://www.sparkproxy.io/",
    impersonate="chrome131",
    proxies={"https": "http://user:pass@proxy.sparkproxy.io:8000"},
)
print(r.http_version, r.status_code)

Setup and version pinning are covered in the curl_cffi scraping guide.

Scrapy needs an explicit handler swap, and its h2 support is still documented as carrying limitations:

# settings.py
DOWNLOAD_HANDLERS = {
    "https": "scrapy.core.downloader.handlers.http2.H2DownloadHandler",
}

Node's undici keeps h2 behind a flag:

import { Agent, request } from 'undici'

const agent = new Agent({ allowH2: true })
const { statusCode } = await request('https://www.sparkproxy.io/', { dispatcher: agent })

HTTP/1.1-only is now the conspicuous choice

For years, sticking to HTTP/1.1 was the safe default. That has inverted. Every current browser negotiates h2 or h3, and the HTTP Archive Web Almanac puts HTTP/2 and HTTP/3 at the clear majority of desktop and mobile requests. A connection that claims to be Chrome and then speaks HTTP/1.1 is now the anomaly.

Three specific ways an HTTP/1.1-only client gives itself away:

The ALPN list

Your ClientHello advertises the protocols you support. A copied Chrome JA3 with an ALPN list offering only http/1.1 is internally inconsistent, and it is visible before a single HTTP byte is exchanged.

Silent downgrade

Some clients offer h2 in ALPN, accept the server's h2 selection, and then a proxy layer or a config flag forces them back to HTTP/1.1 anyway. Servers log the mismatch.

Connection-specific headers

RFC 9113 section 8.2.2 forbids Connection, Keep-Alive, Transfer-Encoding, Upgrade and their relatives in HTTP/2 messages. Browsers stopped emitting them habitually. A hand-built HTTP/1.1 request that still sends Connection: keep-alive next to a modern Chrome User-Agent is announcing that its header set was written by a person, not generated by a browser.

The takeaway is not "always use h2". It is that your protocol version has to be consistent with the identity your other layers claim. An honest client that never pretends to be a browser often survives longer on a permissive target than a half-convincing impersonation.

Proxies and HTTP/2: what passes through

Protocol choice at the proxy layer interacts with h2 in ways that surprise people. Tunnels are transparent. Plain forwarding is not.

HTTPS via CONNECT is fully transparent

An HTTP proxy handling an https:// target opens a tunnel with the CONNECT method (RFC 9110 section 9.3.6) and then relays raw bytes. Your TLS handshake, ALPN negotiation and every h2 frame travel end to end untouched, and the proxy sees only ciphertext. HTTP/2 works, and your h2 fingerprint reaches the origin exactly as your client wrote it. Our explainer on what an HTTP tunnel is walks through the handshake.

SOCKS5 is transparent too

It relays TCP below the HTTP layer, so it has no opinion about protocol versions. The trade-offs between these options are laid out in our guide to HTTP, HTTPS and SOCKS5 proxy protocols.

Plain HTTP forwarding is HTTP/1.1 in practice

For an http:// target, a classic forward proxy receives an absolute-form request target rather than a tunnel. That path is HTTP/1.1 shaped, and you get no h2 to the origin. Scraping plain HTTP through a forward proxy quietly downgrades you.

Check your client, not just your proxy

The tunnel can be perfectly transparent while your own library disables h2 the moment you configure it. Go is the classic trap. The Transport.ForceAttemptHTTP2 field documents that "use of any those fields conservatively disables HTTP/2" when a custom Dial, DialContext, DialTLS or TLSClientConfig is set. Scrapers routinely set TLSClientConfig to pin a cipher list or to skip verification against a MITM proxy, and drop to HTTP/1.1 without noticing:

tr := &http.Transport{
    Proxy:             http.ProxyURL(proxyURL),
    TLSClientConfig:   &tls.Config{MinVersion: tls.VersionTLS12},
    ForceAttemptHTTP2: true, // without this you silently get HTTP/1.1
}

Avoid TLS-terminating middleboxes

Any proxy that decrypts and re-encrypts replaces your ClientHello and re-frames your h2 connection with its own SETTINGS. The origin then fingerprints the middlebox rather than you, and that fingerprint is shared with everyone else behind it.

How to check your own h2 fingerprint

Do not guess. Capture. Three approaches that need no third-party service:

Verify the negotiated version first:

curl --http2 -sv https://www.sparkproxy.io/ -o /dev/null 2>&1 | grep "using HTTP"

Then run a local h2 server and read the frames. nghttp2 ships nghttpd, whose verbose mode prints every SETTINGS, WINDOW_UPDATE, PRIORITY and HEADERS frame your client sends. Point your scraper at it and read the fingerprint fields directly:

nghttpd -v 8443 server.key server.crt

Finally, decrypt a live capture. Set SSLKEYLOGFILE, capture with tcpdump, then load both into Wireshark with the key log configured. That shows the real frames sent to a real target through your real proxy chain, which is the only test that covers the whole stack:

export SSLKEYLOGFILE=/tmp/keys.log
tcpdump -i any -w /tmp/h2.pcap 'tcp port 443'

Record the fingerprint string for every client in your fleet and diff it after each dependency bump. Library upgrades change SETTINGS values, and a fingerprint that quietly shifted is a common cause of "it worked last month".

What to actually use

SituationRecommendation
Simple, permissive targets at high volumeHTTP/1.1 with `requests` or `aiohttp`. No fingerprint pressure, simplest failure modes.
Lossy residential or mobile exitsBenchmark both. Parallel HTTP/1.1 sockets can beat one h2 connection under packet loss.
Targets behind commercial anti-both2, but only with a client that matches a real browser end to end: `curl_cffi` or `curl-impersonate`.
Heavy JavaScript, session-dependent flowsA real browser through Playwright. The fingerprint is genuine because the browser is genuine.
You want none of this to be your problemA scraping API that maintains the handshakes for you.

That last row is the honest answer for most teams. Keeping a hand-built h2 fingerprint aligned with a moving browser release train is continuous work, not a one-time fix. The SparkProxy Scraping API handles TLS and h2 matching, rotation and retries behind one endpoint:

curl "https://scrape.sparkproxy.io/api/v1?url=https://www.sparkproxy.io/&render_js=false&premium_proxy=true&country_code=US" \
  -H "X-API-Key: YOUR_API_KEY"

For hardened targets, add the browser and the extra anti-bot layers:

import requests

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "url": "https://www.sparkproxy.io/pricing",
        "render_js": "true",
        "stealth": "true",
        "premium_proxy": "true",
        "country_code": "US",
        "wait_for": "#pricing-table",
    },
    timeout=120,
)
print(resp.status_code, resp.headers.get("X-Credits-Used"))

Plain HTTP mode (render_js=false) returns X-Job-Id, X-Credits-Used and X-Duration-Ms response headers, which is enough to track cost and latency per target without extra instrumentation. The full parameter reference lives in the SparkProxy Scraping API docs.

Frequently asked questions

FAQ

Usually a little, sometimes not at all. Multiplexing removes per-request connection setup, which helps most on high-latency proxy exits, but on links with meaningful packet loss a single TCP connection suffers head-of-line blocking that parallel HTTP/1.1 sockets avoid. Measure your own target before assuming a win.

It is a compact signature of how a client opens an HTTP/2 connection: the SETTINGS parameters and the order they were sent in, the connection-level WINDOW_UPDATE increment, any PRIORITY frames, and the order of the :method, :authority, :scheme and :path pseudo-headers. Akamai's 2017 research paper defined the format that most anti-bot vendors now use.

Yes for HTTPS targets. An HTTP proxy opens a CONNECT tunnel and relays encrypted bytes, so ALPN and every h2 frame pass through untouched, and SOCKS5 behaves the same way. Plain http:// traffic through a forward proxy is HTTP/1.1 in practice, and some clients disable h2 the moment you set a custom TLS config.

No. requests and urllib3 are HTTP/1.1 only, and urllib3 2.x still ships no HTTP/2 support. Use httpx with http2=True for genuine HTTP/2, or curl_cffi if you also need a browser-matched h2 fingerprint.

Because TLS is only the first of three layers a defender checks. If your JA3 or JA4 says Chrome while your SETTINGS order and pseudo-header order say Go or Python, the combination has never existed in a real browser and a lookup table classifies it instantly. The h2 layer has to match the TLS layer.

Only if your client is not pretending to be a browser. Since every current browser negotiates HTTP/2, an ALPN list offering only http/1.1 alongside a Chrome User-Agent is its own contradiction. Being consistently a script often survives longer than being an unconvincing browser.

Limited-time ยท 50% off

Get 50% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon โ€” claim it before it's gone

Claim Discount

About the Author

The SparkProxy Technical Team builds and operates SparkProxy's proxy infrastructure: datacenter, ISP and residential networks plus the SparkProxy Scraping API. Most of our time goes into the failure modes described above, watching how anti-bot systems classify traffic across the TLS, HTTP/2 and browser layers, and keeping our own handshakes aligned with current browser releases. Technical questions are welcome at support@sparkproxy.io.

Keep reading

Related articles

Headless Chrome vs Headless Firefox for Scraping

Headless Chrome vs Headless Firefox for Scraping

Headless Chrome vs headless Firefox for scraping: new headless mode, CDP vs WebDriver BiDi, memory at concurrency, per-context proxies, and a decision rule.

SparkProxyยทComparisons
cURL vs Python Requests for Web Scraping (2026)

cURL vs Python Requests for Web Scraping (2026)

curl vs Python Requests for web scraping: how TLS fingerprinting, HTTP/2, connection pooling, proxy syntax, and streaming differ, and which to use when.

SparkProxyยทComparisons