← Back to blog
TechnicalAugust 5, 2026·8 min read

Playwright vs. Cloudflare Turnstile: What Bot Detection Actually Looks For

Turnstile decides whether you are automation before your JavaScript runs. Understanding the four layers it inspects is the difference between defending a site properly and guessing.

Headless browser frameworks such as Playwright made automated testing and data collection ordinary. Bot mitigation systems such as Cloudflare Turnstile and reCAPTCHA grew up alongside them, and the two now sit on opposite sides of the same request.

This is written from the defending side. If you operate a site, knowing which signals a mitigation layer reads tells you what your own defences can and cannot see, and where a determined automated agent would still get through. If you write legitimate automated tests against your own site, the same list explains why your suite suddenly started failing a challenge you never configured.

What follows is a map of the signals, a detection check you can run in your own pages, and the architectural measures that hold up. It is not a bypass guide, and section 4 explains why attempting one is a bad trade even before the legal question.

1. Four layers, and only one of them is JavaScript

Turnstile replaces the image puzzle with telemetry [1]. Instead of asking a human to prove themselves, it collects signals across four layers and scores them. The important structural point is that the layers fire in order, and the first one happens before your page exists.

Layer 1: the TLS handshake

Before a single byte of JavaScript executes, the edge inspects the TCP and TLS handshake. The cipher suites offered, the extension list, the elliptic curves, and their ordering in the Client Hello combine into a fingerprint hash, commonly JA3 or JA4 [2]. Automation stacks and proxy nodes routinely produce handshakes that do not match any shipping desktop browser, and a mismatch between that hash and the declared User-Agent header is a strong signal on its own. Nothing you do inside the page can change what already happened at this layer.

Layer 2: browser environment leakage

Playwright drives Chrome over the Chrome DevTools Protocol. That leaves traces. navigator.webdriver reports true unless it is deliberately hidden. Protocol plumbing injects globals with recognizable prefixes. Window dimension metrics can disagree with each other in ways they do not on a real display [3].

Layer 3: telemetry that does not agree with itself

This layer is the interesting one, because it does not look for automation directly. It looks for internal inconsistency. A declared browser signature that claims a platform whose hardware concurrency value it does not have. An empty media device enumeration. A notification permission state that is impossible to reach through the real permission flow. Each is individually weak; together they describe an environment that was assembled rather than booted [3].

Layer 4: interaction dynamics

Human pointer movement has a characteristic shape: curved paths, non-linear acceleration, overshoot and correction near the target. Programmatic clicks arrive at exact coordinates with no approach at all, and straight-line interpolation between two points has a velocity profile no hand produces [4].

2. A detection check you can run yourself

You do not need a vendor to inspect the layer-2 and layer-3 signals in your own pages. The check below collects anomalies rather than returning a single boolean, which is the right shape: one signal is noise, several together are a finding.

/**
 * Browser-side automation signal check.
 * Collects anomalies instead of deciding on any single one.
 */
async function inspectAutomationEnvironment() {
    const anomalies = [];

    // 1. The WebDriver flag itself
    if (navigator.webdriver) {
        anomalies.push("navigator.webdriver is true.");
    }

    // 2. DevTools Protocol artifacts left on the global object
    const cdpLeak = Object.keys(window).some(key =>
        key.startsWith("cdc_") || key.startsWith("__puppeteer") || key.startsWith("__playwright")
    );
    if (cdpLeak) {
        anomalies.push("DevTools Protocol artifacts present on window.");
    }

    // 3. A permission state pair that the real flow cannot produce
    if (navigator.permissions) {
        try {
            const status = await navigator.permissions.query({ name: "notifications" });
            if (Notification.permission === "denied" && status.state === "prompt") {
                anomalies.push("Inconsistent notification permission state.");
            }
        } catch (e) {
            // Unsupported in some browsers; absence is not a signal
        }
    }

    // 4. Telemetry that a real device always reports
    if (!navigator.languages || navigator.languages.length === 0) {
        anomalies.push("navigator.languages is empty.");
    }
    if (navigator.hardwareConcurrency === undefined || navigator.hardwareConcurrency < 1) {
        anomalies.push("Missing or invalid hardwareConcurrency.");
    }

    return anomalies.length > 0
        ? { automated: true, reasons: anomalies }
        : { automated: false };
}

window.addEventListener("DOMContentLoaded", async () => {
    const result = await inspectAutomationEnvironment();
    if (result.automated) {
        console.warn("Automation signals detected:", result.reasons);
    }
});

Treat what this returns as a score, not a verdict. Every check above has a false positive: privacy browsers and hardened configurations suppress telemetry deliberately, and assistive technology can produce interaction patterns that look nothing like a moving cursor. Blocking on one anomaly means blocking real people. That is why this belongs in logging and risk scoring, not in an if statement that returns 403.

Note also what this check cannot see: layer 1 happened at the edge, and layer 4 needs a session's worth of events. Anything running inside a single page load only ever sees the middle of the picture.

3. Why the signals combine rather than stack

The reason multi-layer detection works is that the layers are hard to make consistent with each other. Hiding navigator.webdriver is a one-line change. Making a hidden navigator.webdriver agree with a plausible TLS fingerprint, a coherent hardware profile, and human-shaped pointer dynamics is a much harder problem, because each thing you change to satisfy one layer tends to introduce an inconsistency in another.

This is the same structural insight that makes browser fingerprinting research work in the first place [3]: what identifies a browser is rarely one attribute, it is the joint distribution over many. Detection and identification are the same mathematics pointed in different directions.

4. Why bypass attempts are a bad trade

The technical risks arrive before the legal ones, and they land on people other than you:

  1. Subnet and ASN blocking. Repeated failed challenges get whole IP ranges and datacenter autonomous system numbers blocked, not individual sessions.
  2. Reputation damage to shared infrastructure. Inconsistent fingerprint spoofing degrades the reputation score of the network you are on. The visible outcome is that unrelated people on that network start getting aggressive challenges.
  3. Legal exposure. Circumventing an access control without authorization can breach a site's terms of use and, depending on jurisdiction and what data is involved, raise claims under laws such as the US Computer Fraud and Abuse Act or data protection regimes including the GDPR [5].

If you need automated access to a site you do not operate, the supported paths are an API, a documented data export, or asking. If you are testing your own site, allowlist your test infrastructure at the edge, which is a configuration change rather than an adversarial problem.

5. Defending a site, in order of what actually helps

  1. Verify the fingerprint against the declared identity. Configure edge rules so an incoming JA3 or JA4 hash has to be consistent with the User-Agent the request claims [2]. This is the cheapest high-signal check available, and it runs before your application does.
  2. Rate limit by what the traffic is, not just how much of it there is. Apply limits keyed on session token, ASN type, and subnet. Datacenter ranges warrant stricter default challenge modes than residential ones.
  3. Validate session consistency over time. A session whose TLS signature, subnet, or browser fingerprint changes mid-flight is either compromised or synthetic. Invalidate it. This catches things no single-request check can, because it uses the one signal automation finds hardest to fake: continuity.
  4. Score, log, and challenge rather than block outright. A hard block on a heuristic is a promise to lock out some fraction of real users, and you will not hear from most of them. Graduated responses give you the same protection and a feedback loop.

Closing

Bot detection is not one clever check, it is four different vantage points that are individually weak and jointly difficult to satisfy at once. That is worth understanding whichever side of the request you sit on: it tells a defender where to spend effort, and it tells anyone writing automation why the ninth patch to hide a WebDriver flag is not going to be the one that works.

References

  1. Cloudflare. (2022). Turnstile: A privacy-first alternative to CAPTCHA. developers.cloudflare.com/turnstile
  2. Anderson, B., & McGrew, D. (2017). TLS Beyond the Browser: Combining End Host and Network Data to Understand Application Behavior. Proceedings of the ACM Internet Measurement Conference. See also the JA3 and JA4 fingerprint specifications.
  3. Vastel, A., Laperdrix, P., Rudametkin, W., & Rouvoy, R. (2018). FP-STALKER: Tracking Browser Fingerprint Evolutions. Proceedings of the 39th IEEE Symposium on Security and Privacy (S&P), 728–741.
  4. Iscı, S., & Şahin, G. (2021). Mouse movement dynamics for bot detection in web applications. Journal of Information Security and Applications, 58, 102765.
  5. Krotov, V., & Silva, L. (2018). Legality and Ethics of Web Scraping. Communications of the Association for Information Systems, 42(1), 539–563.

Want to share your own experience? Every member can write here: reach out and we'll help you publish your first post.