Scraping Central is reader-supported. When you buy through links on our site, we may earn an affiliate commission.

4.34advanced5 min read

TLS / HTTP/2 Fingerprint Rotation

Below HTTP, TLS and HTTP/2 reveal which library is calling. The tooling that makes Python and PHP look like real browsers at the wire level.

What you’ll learn

  • Understand JA3 / JA4 / Akamai HTTP/2 fingerprints.
  • Use curl-cffi, tls-client, or curl-impersonate to spoof TLS.
  • Decide when TLS spoofing is necessary vs overkill.

Before any HTTP byte is sent, the TLS handshake exposes a fingerprint. Cloudflare, Akamai, DataDome, and others profile the TLS ClientHello and the HTTP/2 SETTINGS frame. A scraper with perfect Chrome headers but a Python TLS library still flags as a bot, at the TLS layer.

What's in a TLS fingerprint

The ClientHello packet contains:

  • TLS version range (e.g. 1.0–1.3).
  • Cipher suites in client preference order.
  • Extensions (server_name, supported_groups, key_share, ALPN, signature algorithms, etc.).
  • Elliptic curves (supported groups).
  • Point formats.

These are hashed into a JA3 string. JA4 is the newer (2023+) format that also captures TLS extension order and ALPN values.

Each library produces a distinct JA3:

  • Chrome 120 on Mac: a specific hash.
  • Safari 17 on iOS: a different hash.
  • Python httpx + OpenSSL: a hash easily flagged as "scripting library."
  • PHP curl: yet another.

HTTP/2 SETTINGS fingerprint

HTTP/2 connections begin with a SETTINGS frame whose values, order, and the subsequent HEADERS frame's HPACK compression all vary by client. Akamai uses this to fingerprint clients.

For example:

  • Chrome SETTINGS: HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144.
  • curl SETTINGS: different values and order.
  • Python http2: different again.

The tooling that lets you spoof

Native Python and PHP cannot easily customize the TLS stack, they use OpenSSL or NSS underneath, which have their own ClientHello structure. Special tools exist:

Python: curl-cffi

from curl_cffi import requests

session = requests.Session(impersonate="chrome120")
r = session.get("https://practice.scrapingcentral.com/challenges/antibot/tls-fingerprint")

impersonate="chrome120" makes the underlying libcurl produce Chrome 120's exact TLS handshake. Variants: chrome116, chrome119, chrome120, safari17_0, safari17_2, edge99, edge101, firefox120, etc.

Drop-in replacement for requests.Session() for most use. Adds JA3, JA4, HTTP/2 SETTINGS, and even TLS extension order.

Python: tls-client

import tls_client

session = tls_client.Session(
  client_identifier="chrome_120",
  random_tls_extension_order=False,
)
r = session.get(url)

Similar concept; a pure-Python (with native binary) implementation. Some teams prefer it for license reasons.

Python: playwright (just use a browser)

If you're already running Playwright for JS, the browser handles TLS naturally. No fingerprint spoofing needed; the browser IS the fingerprint.

PHP: curl-impersonate

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Use the curl-impersonate binary that ships with Chrome's TLS handshake
exec("curl_chrome120 -s '$url'", $output);

Native PHP curl uses system OpenSSL; you can't reach into the ClientHello from PHP code. The workaround: shell out to the curl-impersonate binary or proxy through a Node.js / Python service that handles the TLS layer.

For higher integration, azimov/php-curl-impersonate-cffi and similar projects exist; results vary.

PHP: Symfony Panther / Chrome via WebDriver

For PHP scrapers needing serious anti-bot, running Chrome through Panther is more reliable than wrestling with TLS at the protocol level. Pay the browser cost.

When TLS spoofing is necessary

Decision points:

Target tier TLS spoofing needed
Internal API, partner sites No
Static catalogue, no anti-bot No
Mid-tier e-commerce Sometimes, try without first
Cloudflare with TLS-aware bot detection Often yes
Major SERP (Google, Bing) Yes
Akamai-protected (banking, airline) Yes, plus more

For most projects, header coherence + residential IPs is enough. TLS spoofing comes in when you've matched everything at higher layers and still get blocked.

A complete request with TLS spoof

from curl_cffi import requests
import random

UAS = [
  ("chrome120", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
  ("safari17_0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"),
]

profile, ua = random.choice(UAS)

session = requests.Session(impersonate=profile)
session.headers.update({
  "User-Agent": ua,
  "Accept-Language": "en-US,en;q=0.9",
})
r = session.get("https://target.com/page", proxies={"https": "http://...residential..."})

TLS, HTTP/2, headers all aligned. The result is indistinguishable from a real browser at the network layer.

What spoofing doesn't fix

TLS fingerprint spoofing handles the wire layer. It does NOT help with:

  • JavaScript challenges (you need an actual JS engine, Playwright).
  • CAPTCHA (separate solving, §4.38–§4.40).
  • Behavioral telemetry (mouse, scroll patterns).
  • Canvas/WebGL fingerprints (require GPU rendering).

For those, you still need a real or headless browser. TLS spoofing is the foundation that lets requests-style scrapers blend in for non-JS targets.

HTTP/2 considerations

When using HTTP/2, the SETTINGS fingerprint is checked. curl-cffi handles this; raw httpx with HTTP/2 enabled has its own SETTINGS signature that's distinguishable.

Some targets force HTTP/2 even for unauthenticated requests; some downgrade to HTTP/1.1 for older-looking clients. Test which path the target prefers.

Testing your fingerprint

Use a self-fingerprinting endpoint:

  • https://tls.peet.ws/api/all, returns your JA3, JA4, HTTP/2 fingerprint, headers.
  • https://browserleaks.com/tls, interactive fingerprint inspector.

Hit them from your scraper. Compare your JA3 to a real Chrome's. If they match (or land on a "Chrome-like" list), your spoofing is working.

Hands-on lab

Against /challenges/antibot/tls-fingerprint:

  1. Hit it with default httpx, record the response.
  2. Hit it with curl-cffi impersonating Chrome 120, record the response.
  3. Hit https://tls.peet.ws/api/all from both to see the JA3 difference.

You'll see the layer change. The TLS challenge passes only when the wire-level signature matches a real browser. Header coherence alone isn't enough.

Hands-on lab

Practice this lesson on Catalog108, our first-party scraping sandbox.

Open lab target → /challenges/antibot/tls-fingerprint

Quiz, check your understanding

Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.

TLS / HTTP/2 Fingerprint Rotation1 / 8

What is a JA3 fingerprint?

Score so far: 0 / 0