Playwright detected as a bot on one site: a checklist
A troubleshooting order for when automation gets a different page than a human does. It is written for Playwright but almost none of it is Playwright-specific.
The order matters. Most people start at the bottom of this list, buy a better proxy, and find out three days later that the problem was a user agent they set themselves.
0. Prove it is actually detection
Before anything else, open the same URL by hand from the same machine and the same network. Not from your laptop, from the machine that runs the automation.
If the manual visit also gets the wrong page, this is not a browser fingerprint problem: it is the IP, the ASN, or the country. Nothing in the rest of this list will help, and the fix is a different exit.
If the manual visit works and the automated one does not, the difference is the browser or the behaviour, and the list below is in the order that difference usually lives.
1. Check what you overrode yourself
This is the most common cause and the most embarrassing one.
Every value you set by hand is a value that now has to agree with every value you did not. A user agent claiming one platform, on a browser reporting another. A timezone you pinned, against an IP on a different continent. A language list from your own machine, on an exit somewhere else.
Grep your own code for user_agent, locale, timezone_id, viewport, extra_http_headers and any stealth plugin you installed. Then remove all of them and try again. If it starts working, add them back one at a time and stop at the one that breaks it.
Detectors rarely check whether a value is unusual. They check whether two values that should agree, do.
2. Check whether you are running two spoofers at once
If you use a patched browser or a stealth build and a JavaScript stealth plugin, they are both answering the same questions and they will not give the same answer. Two disguises produce a contradiction that neither produces alone.
Pick one layer. If the engine handles the fingerprint, turn off the page-level patching and any header generator the framework offers.
3. Check the machine, not the browser
Open a fingerprinting page and read the report instead of the verdict. The things that most often give away a server:
- A software WebGL renderer. If the GPU string mentions a basic, software or llvmpipe renderer, the browser has announced it is running somewhere without graphics hardware. No amount of property patching hides that. Worse, and less obvious: the string can say NVIDIA while the pixels are still drawn by a software rasterizer, which is a mismatch you cannot patch.
- A screen that does not exist. Odd resolutions, a device pixel ratio that no real display has, an available height equal to the full height, meaning no taskbar.
- Font lists that do not match the claimed platform. Claiming Windows with a Linux font set is a one-line check.
- Hardware concurrency and device memory that pair oddly with the claimed machine.
- No audio device. Sample rate, output latency and channel count come from real hardware, and a container that has none answers with defaults that say so. See AudioContext fingerprinting.
- An empty speech voice list.
speechSynthesis.getVoices()returns what the operating system has installed, so an empty array under a Windows user agent is a server saying it is a desktop. See why getVoices() comes back empty.
None of these are automation flags. They are all “this is a datacenter” flags, and they survive every stealth plugin ever written because they are not in JavaScript’s gift to change.
4. Only now, check the automation tells
navigator.webdriver, leftover automation globals, a headless user agent, a missing or malformed chrome object on a Chromium build. These are worth checking and they are almost never still the problem in 2026, because every tool fixes them first.
If you find one, note that setting it to false is not the fix: a clean browser reports undefined, and false is a value with its own signature.
5. Check the behaviour
Some sites do not fingerprint at all, they watch.
A pointer that jumps from coordinate to coordinate without passing through the space between. Keystrokes at a perfectly uniform interval. A form filled in eighty milliseconds. A page that is never scrolled. A session with no mouse movement at all before a click.
If the block appears only after an interaction, and only on some pages, this is the one. It also explains blocks that arrive minutes into a session rather than at the first request.
6. Check the network layer
TLS fingerprints, HTTP/2 settings frames, header order. A real browser’s handshake is distinctive and a mismatch between “I am Firefox” in the user agent and a handshake that is not Firefox’s is decisive.
You cannot fix this from JavaScript. Either the browser is real, or the request is made by something that impersonates the handshake too.
While you are here, check what the browser says about the network on its own account. WebRTC through a proxy is the common one, and it fails in both directions: it can report your real address next to the proxy’s, or report nothing at all, which is its own signal.
7. Now consider the exit
If everything above is clean and it still fails, the IP is a reasonable next suspicion. Datacenter ranges, an ASN with a bad reputation, an exit that a thousand other people are using this minute, a country that does not match the rest of the session.
That last one is worth separating out, because it is not really about the IP being bad. It is about the browser and the exit telling different stories, and it is cheap to fix: see when the timezone does not match the proxy for everything that has to agree.
It is seventh on this list, not first, because it is the expensive fix and the least likely single cause.
The debugging habit that saves the most time
Fix the identity so a failure is reproducible.
If every run gets a different fingerprint, a failing run tells you nothing: you cannot tell the site changing from the machine changing. Whatever tool you use, find its way to pin the identity across runs, and change it deliberately rather than accidentally.
It is the single biggest difference between debugging this and guessing at it, and it is why this project derives every surface from one seed: the same seed gives the same machine every time, so a bisect is a bisect.
Short answers to the questions that lead here
Why does Playwright work on every site except one? Because that site checks something the others do not, and it is usually about the machine rather than the automation. Work the list above in order.
Is it the proxy? It is seventh on the list for a reason. It is the expensive fix and the least likely single cause, and people buy a better proxy before checking the free things.
How do I tell detection from a normal failure? Compare the response against what a human browser gets on the same URL. A different page, a challenge, or a body that is suspiciously short are detection. A timeout is often just a timeout.
Does headless mode itself get detected? Less than people think. What gets detected is what tends to come with it: no GPU, no fonts, no audio device, a screen size nobody has.
Should I randomise the user agent? No. It has to agree with the engine, the platform, the fonts and everything else. Randomising it alone creates contradictions rather than hiding anything.
See also: WebGL renderer strings and why headless renders different fonts for step three, and what sannysoft actually checks before you trust a green table.
Written while maintaining invisible_playwright, a Firefox patched at the C++ level driven by stock Playwright. Steps 1, 2, 3 and 5 above are all mistakes I have made.