A website can tell whether your request comes from Chrome, Node.js, or curl before it reads a single HTTP header. The identification happens during the TLS handshake, the encrypted connection setup that precedes any application data. A new open-source tool called TLS Forge makes this hidden layer measurable and reproducible from Go, Node.js, and Python.

The problem is familiar to anyone who has scraped a protected site or built automation against a service with anti-bot measures. You copy the Chrome User-Agent, add the Accept header, replicate every Sec-Fetch-* value, and the server still returns a 403 or a challenge page. The reason is that the User-Agent is part of the HTTP request, which arrives after the TLS handshake. The server has already classified your client before it sees those headers.

What a TLS fingerprint actually measures

When a new HTTPS connection starts, the client sends a ClientHello message listing the TLS versions, cipher suites, extensions, supported groups, signature algorithms, and ALPN protocols it supports. The selection and ordering of these fields depend on the application, its version, and its underlying TLS library. Chrome uses BoringSSL. Node.js uses OpenSSL. The differences show up in the handshake itself.

The best-known fingerprint formats are JA3 and JA4. JA3 produces a string of five numeric identifier groups, TLSVersion, CipherSuites, Extensions, SupportedGroups, and PointFormats, hashed with MD5. MD5 here is not for security; it is a compact lookup key. The format has a significant weakness: starting with Chrome 110, the browser permutes TLS extensions on every new connection to prevent intermediaries from relying on a fixed order. The same browser version produces different JA3 values in succession.

JA4 addresses this by producing a human-readable prefix followed by truncated SHA-256 hashes. A measured JA4 might look like t13d1516h2_8daaf6152771_806a8c22fdea. The prefix encodes the TLS version, cipher suite count, extension count, and ALPN protocol without requiring a hash reversal. The hashes operate on sorted cipher and extension lists with GREASE values excluded. Because the lists are sorted, Chrome's extension permutation usually does not change the JA4 value.

Neither JA3 nor JA4 is a permanent identifier. Browser updates change the advertised features. When a TLS 1.3 session resumes with a pre-shared key, the ClientHello includes a pre_shared_key extension that changes the extension count and the complete JA4. These fingerprints describe a client type and configuration, not an individual person. Many devices running the same browser version share the same fingerprint.

The full profile matters more than one hash

A server does not stop at the TLS fingerprint. With HTTP/2, it observes SETTINGS frames, connection window updates, and the order of pseudo-headers like :method and :path followed by regular headers. HTTP/1.1 has no pseudo-headers, but the order and casing of header names are measurable. Chrome, for example, sends a single Host field first.

Security systems combine these signals with cookies, IP reputation, session behavior, and JavaScript checks. A request that claims User-Agent: Chrome while sending a ClientHello from a different library and unusual HTTP/2 settings presents a contradictory identity. Consistency across the entire profile, TLS, HTTP/1.1, HTTP/2, and headers, is what matters.

Changing the User-Agent in Node.js does not fix this because the standard TLS transport uses OpenSSL, not BoringSSL. The public Node.js TLS API can configure cipher suites, TLS versions, ALPN, signature algorithms, and key-share groups. Starting with Node.js 26.4.0, it can also enable certificate compression. But it still does not provide full control over the extension set and order, Chrome's permutation behavior, or the interaction between all these choices on the wire. A close ClientHello is still insufficient if HTTP/2 behavior and header order remain characteristic of a different client.

How TLS Forge captures and reproduces a profile

TLS Forge launches an installed Chromium-based browser and measures TLS, HTTP/1.1, HTTP/2, and headers on a local server through two independent cold navigations. Separate navigations prevent one measurement from changing the Sec-Fetch-* context of the other. Every measurement starts with a fresh browser profile and a new connection. A resumed TLS session contains pre_shared_key and no longer describes the first connection, so TLS Forge rejects such captures as profile input.

The result is stored as a JSON profile. A Go transport based on uTLS uses that profile for outgoing requests. Node.js and Python SDKs delegate to the same transport, so their result does not depend on OpenSSL or the standard HTTP client in either language. The compare command sends a browser and TLS Forge to the same local measurement server and reports differences in TLS, JA4, HTTP/1.1, HTTP/2, and headers. After a browser update, a new capture shows exactly what changed.

Built-in profiles for Chrome 152 on macOS, Linux, and Windows include measured HTTP/1.1 sections. The chrome alias selects the newest measured profile for the current platform. The tool includes a cookie jar, cookie import and export, HTTP, HTTPS, and SOCKS proxy support, and connection reuse.

What it covers and what it does not

TLS Forge reproduces TLS over TCP, HTTP/1.1, HTTP/2, and headers as one coherent browser profile. It does not measure HTTP/3 or QUIC, the TCP/IP fingerprint, DNS behavior, latency, or packet patterns. It does not execute JavaScript received from the target site, solve CAPTCHAs, improve IP reputation, or imitate Canvas, WebGL, fonts, or user behavior. The capture command launches Chromium and uses a local measurement page, but this does not turn the fetch, batch, proxy, or SDK interfaces into browser automation.

For web scraping, this means TLS Forge removes a network-profile mismatch but does not guarantee a request will pass Cloudflare protection. The result depends on IP reputation, cookies, request history, JavaScript checks, and client behavior. Cloudflare's Bot Management considers JA3 and JA4 alongside these other signals. TLS Forge is useful for diagnosing anti-bot filters, integration testing, and researching your own security controls. Use it only with systems and data you are authorized to access.

The tool is available as a command-line tool, a Go library, and Node.js and Python SDKs that communicate with a long-running daemon over a JSON Lines protocol. The basic workflow takes three commands: capture a profile from a browser, compare it against the live browser to verify accuracy, and use the profile for requests. For developers whose scrapers or automation tools keep getting blocked despite correct headers, the problem is almost certainly in the TLS handshake, not the HTTP layer.