January 22, 2026·Security Deep Dive·9 min read

The Economics of Bot Wars: How CAPTCHAs, Fingerprinting, and Bypass Strategies Shape the Modern Web

By Amber Bisht, Full-Stack & Systems Engineer

Part 1/2
0:00 / 8:45

If you look at the raw request logs of any major web platform today, you'll see a quiet, ongoing war. Automated bot traffic now accounts for more than half of all internet activity, according to recent bad-bot telemetry. The web has quietly become a machine-majority landscape, and every major platform has had to build infrastructure to cope with it.

To protect infrastructure, prevent ad revenue fraud, stop credential stuffing, and keep free API tiers from being drained, platforms deploy Web Application Firewalls and anti-bot systems — Cloudflare Turnstile, Akamai Bot Manager, hCaptcha, Datadome, and others. In this piece, we look at how these defense layers work mechanically, how modern automation gets around them, and why bot defense ultimately comes down to economics rather than pure cryptography.

Edge validation happens in layers

When a connection hits a WAF-protected edge server, validation runs sequentially from the transport layer up through the application layer, long before your backend ever sees the request:

  • L4 Transport — a TCP SYN check (p0f-style) verifying TTL and window size at the kernel level.
  • L5 Session — the TLS Client Hello, where a JA4 fingerprint is extracted and GREASE ciphers are validated.
  • L7 Application — HTTP/2 SETTINGS frames, checked for stream priority tree structure and initial window updates.
  • Runtime (V8) — JavaScript DOM telemetry that inspects browser prototypes and WebGL hardware reporting.

Each of these layers catches a different class of forged client. Let's go through them one at a time.

TCP/IP fingerprinting happens before a single byte of TLS is read

Before any encrypted payload is touched, a WAF can already profile a connection using Passive OS Fingerprinting, or p0f. During the three-way TCP handshake, the client's SYN packet reveals its initial TTL (Linux typically ships 64, Windows 128), its maximum segment size, its initial window size, and the order in which it lists TCP options like MSS, window scale, SACK-permitted, NOP, and timestamps.

This is why header spoofing alone doesn't work. If a scraper sets its headers to claim it's Chrome on Windows 11, but its SYN packet carries a TTL of 64 and Linux-ordered TCP options, the WAF can drop the connection at the kernel layer before the forged headers are ever inspected.

TLS fingerprinting: JA3 gave way to JA4

Once the TCP socket is up, the client sends a TLS Client Hello containing its cryptographic preferences — and this, too, is a fingerprint. JA4 succeeded the older JA3 scheme with a more structured, human-readable format built as three parts: JA4a, JA4b, and JA4c.

JA4a encodes the transport and negotiated protocol — for example, TCP, TLS 1.3, SNI presence, cipher count, extension count, and ALPN (HTTP/2). JA4b is a SHA-256 hash of the client's supported cipher suites, sorted alphabetically so that reordering doesn't create false mismatches. JA4c is a similar hash, but of the TLS extensions and signature algorithms.

One detail trips up a lot of scraping tools: GREASE. Chromium-based browsers deliberately inject meaningless placeholder values into their cipher and extension lists — a mechanism designed to keep the protocol extensible without breaking on unexpected values. Standard HTTP client libraries like axios or Python's requests don't do this. So when a request shows up with a Chrome user-agent but no GREASE values in its TLS handshake, that mismatch alone is often enough to flag it instantly.

HTTP/2 has its own fingerprint too

If the connection negotiates HTTP/2, the client sends a SETTINGS frame right after the connection preface — and the exact values and ordering in that frame are characteristic of the client software that generated them. Real Chromium sends a specific, consistent profile (header table size, push settings, max concurrent streams, initial window size, and so on), produced by its underlying nghttp2 engine.

text
[SETTINGS_HEADER_TABLE_SIZE: 65536]
[SETTINGS_ENABLE_PUSH: 0]
[SETTINGS_MAX_CONCURRENT_STREAMS: 1000]
[SETTINGS_INITIAL_WINDOW_SIZE: 6291456]
[SETTINGS_MAX_FRAME_SIZE: 16384]
[SETTINGS_MAX_HEADER_LIST_SIZE: 262144]

Anti-bot systems check the order and values of these settings, the structure of the HTTP/2 stream priority tree used to sequence asset loading, and how WINDOW_UPDATE frames adjust flow-control capacity over time. Automation frameworks that build HTTP/2 requests from scratch, rather than through a real browser engine, tend to produce settings frames that don't match any known browser — another tell.

As one security researcher put it: bot defense isn't a mathematical cryptography puzzle. It's a game of computational and financial economics.

When the network layer passes, the browser gets interrogated

If a connection clears the network-level checks, many WAFs still serve a client-side JavaScript challenge that runs inside V8 and probes the browser environment directly.

This looks for automation artifacts left behind by tools like Chrome DevTools Protocol drivers — properties such as navigator.webdriver, CDP-injected globals, or DOM properties prefixed with __webdriver, __selenium, or $cdc_. It also checks whether native methods have been tampered with: if a script has patched something like document.createElement using Object.defineProperty or a Proxy, calling Function.prototype.toString on it won't return the expected [native code] signature. Some scripts even parse the JavaScript error stack trace looking for file paths that reveal Puppeteer or Playwright.

Hardware-level checks round this out. Canvas fingerprinting draws hidden shapes and text and hashes the resulting pixels, which vary subtly by GPU and anti-aliasing driver. WebGL renderer queries can reveal software renderers like SwiftShader or LLVMpipe, which are common inside virtual machines. And font enumeration measures the pixel bounds of fallback fonts rendered in a hidden iframe, which differs across real operating systems.

How the other side responds

Operators building large-scale automation respond to each of these layers directly. Low-level network spoofing tools — compiled in Go or Rust, such as tls-client or curl-impersonate — reproduce a real browser's TLS cipher list, extension ordering, GREASE distribution, and HTTP/2 settings frame without the overhead of actually rendering a page. Browser-patching tools like Rebrowser modify Chromium at the V8 debugger level before any page script runs, which removes the prototype-tampering traces and CDP indicators that DOM telemetry checks look for.

Proxies & IP reputation hierarchy

Then there's IP reputation, which is arguably the more decisive factor in practice. WAFs score incoming requests partly by the autonomous system network the IP belongs to:

Proxy typeCostASN typeBlock riskTypical use
Datacenter$0.10–$0.50/GBHosting providers (AWS, DigitalOcean, OVH)Very highHigh-speed API queries on lightly protected targets
Residential$2–$12/GBConsumer ISPs (Comcast, BT)MediumStandard web scraping and search
Mobile LTE$5–$20/GBCellular carriers (AT&T, Verizon)Very lowBypassing strict logins and CGNAT-protected endpoints

Mobile proxies are the hardest to block for a structural reason: carrier-grade NAT means a single public IPv4 address is shared by thousands of real devices at once. Blocking a mobile IP risks blocking a huge number of legitimate users along with it, so WAFs are systematically more lenient toward cellular ASNs.

Building defenses that hold up

None of this means defenders are stuck. A few architectural choices meaningfully raise the cost of automation:

  • Push validation to the edge. Serving JS challenges and validating TLS signatures inside edge workers (Cloudflare Workers, AWS CloudFront Functions) keeps this load off your database and application tier entirely.
  • Cross-check identity claims.Verify that a request's TLS fingerprint actually matches the user-agent it claims — a "Chrome" request arriving with Python's TLS cipher list should be dropped at the edge, not deeper in the stack.
  • Plant honeypots in the DOM. Hidden fields that real users never interact with, but that naive HTML-parsing scrapers will happily fill in or click, are a cheap and effective tripwire.
  • Use edge-validated session cookies. Encrypted state flags checked continuously through a session make it expensive for automation to simply replay a single successful handshake indefinitely.

The throughline across all of this is that no single layer is decisive on its own. TCP fingerprinting, TLS fingerprinting, HTTP/2 fingerprinting, and JS-level telemetry each catch a different class of mismatch, and serious anti-bot systems layer them together. Automation, in turn, has to get every layer right simultaneously — which is precisely why this remains a game of raising the other side's cost, not a puzzle with a final, permanent solution.