Your headless Chrome works perfectly on your laptop, then starts getting 403s and challenge pages the moment it runs on a server. The browser itself is leaking that it is automated, and no amount of retry logic fixes that. By the end of this you'll know which signals give a headless browser away, what puppeteer stealth actually patches, where it still gets caught, and when to stop patching and offload the render entirely.
Puppeteer stealth is the usual first fix, and it is a real one. But it solves a specific slice of the problem, and the parts it leaves open are exactly the parts modern anti-bot systems lean on hardest. There is a reason a fully stealthed browser still trips Cloudflare, and we'll get to it.
If the screenshot is the thing you actually need, not a step inside a test you control, you can skip this entire fight. A rendering API like ScreenshotRender runs the browser on its own infrastructure with bot-detection handling built in, so a URL goes in and a clean image comes out. We'll come back to where that wins. First, why the blocking happens at all.
Why does headless Chrome get blocked?
Headless Chrome gets blocked because it sends signals a normal human browser never sends, and anti-bot systems score those signals to decide who is automated. No single signal is conclusive, so detectors stack many of them and weigh the total. A vanilla automated browser fails on several at once.
The signals fall into a few groups:
- The WebDriver flag. Automation sets
navigator.webdrivertotrue, which is the single most-read tell. A two-line script catches it. - Headless and automation tells. The default headless user agent used to literally contain
HeadlessChrome, and an automated browser often has an empty plugins list, a missingchrome.runtimeobject, and launch flags like--enable-automationthat a real user never sets. - Fingerprint mismatches. Canvas, WebGL vendor, fonts, screen size, and hardware concurrency all get hashed into a fingerprint. Server-rendered browsers tend to share one suspiciously identical fingerprint across thousands of requests.
- Network and behavior. The TLS handshake, the IP address, and the complete absence of mouse movement or realistic timing all feed the same score, and none of them live in JavaScript.
Stealth tools exist to scrub the first two groups, the JavaScript-level tells. That is exactly where puppeteer stealth comes in, and also where its ceiling sits.
What is puppeteer stealth and what does it actually patch?
Puppeteer stealth is puppeteer-extra-plugin-stealth, a plugin that applies a stack of patches to make a Puppeteer-driven Chrome look like an ordinary browser. You install puppeteer-extra alongside the stealth plugin, register it once, and it rewrites the tells before your page ever loads.
Under the hood it sets navigator.webdriver back to undefined, fakes a realistic plugins and mimeTypes list, strips HeadlessChrome from the user agent, restores the chrome.runtime object, and patches WebGL vendor and permission queries so they match a real Chrome. Each patch closes one check that a naive detector relies on, and together they clear the common open-source "are you a bot" test pages.
For a long time that was enough. Run vanilla Puppeteer against a protected site and you get a challenge; add the stealth plugin and the same script sails through. The trouble starts when the detector on the other side is doing more than reading JavaScript, which is increasingly the default.
What about playwright stealth and undetected-chromedriver?
Playwright stealth and undetected-chromedriver are the same idea ported to other stacks: playwright stealth patches a Playwright browser, and undetected-chromedriver patches the ChromeDriver that Selenium uses. Whatever your automation library, there is a stealth layer shaped like the one Puppeteer has.
On the Playwright side you reach for playwright-extra with the same stealth evasions, or a community playwright-stealth package in Python. On the Selenium side, undetected-chromedriver auto-patches the driver binary to strip the cdc_ variables that ChromeDriver injects and that scanners look for first. The library you pick rarely changes the outcome, since the detection surface is the browser, not the bindings, and the same trade-off shows up when you are choosing between Puppeteer and Playwright for screenshots in the first place.
Skip the stealth treadmill. Just get the screenshot.
ScreenshotRender runs the browser on its own infrastructure with bot-detection handling built in, so you don't maintain a stealth stack across every Chrome release. Pass a URL, get a clean PNG. 100 free screenshots, no credit card.
Try a renderWhy does puppeteer stealth still get blocked?
Puppeteer stealth still gets blocked because it only patches JavaScript-level tells inside the page, while modern anti-bot systems also inspect signals that never touch JavaScript. You can hide every property a script can read and still fail on the parts the stealth plugin cannot reach.
- TLS fingerprint. Your HTTPS handshake has a signature (the JA3 fingerprint) that reveals the real client library, and a stealth plugin patches page JavaScript, not the network stack underneath it.
- IP reputation. Datacenter ranges from the major clouds are flagged on sight, so a perfectly disguised browser running on an EC2 box still looks wrong before the page even renders.
- Behavior. No mouse movement, instant navigation, and mechanical request timing read as a script regardless of how clean the fingerprint is.
- Managed challenges. Systems like Cloudflare bot management combine all of the above into one score and serve a challenge when it crosses a threshold, which is the Cloudflare-specific version of this same fight.
Even a setup that handles all four is a maintenance treadmill. Every Chrome release can add a new tell, and every anti-bot model update can re-expose one you thought you had covered, so the stealth stack that worked last month quietly starts failing this month.
When should you stop patching and use a screenshot API instead?
You should stop patching when the screenshot is the deliverable, not a step inside a flow you control, because keeping a stealth stack alive across Chrome updates and anti-bot vendors turns into a second job. Puppeteer is built to drive a browser through actions; when all you want is a clean image of a public URL, most of the stealth work is overhead you are maintaining for one frame.
With ScreenshotRender the whole request is one line: https://screenshotrender.com/api/v1/screenshot?apiKey=YOUR_API_KEY&url=https://nowsecure.nl&fullPage=true. The url parameter is the page you want and fullPage=true captures the whole scrollable document, while the browser, the rendering, and the bot-detection handling all run on our side rather than yours.
The relevant feature is Stealth Mode, which ships on the Hobby plan and above and is built to bypass bot-detection and anti-scraping measures, so you are not the one patching navigator.webdriver across releases. On every plan, including the free one, cookie consent banners, ad overlays, and chat widgets are removed before the capture, and a render that fails does not cost a credit, so you only pay for shots that actually come back. If you are weighing providers, we broke down what separates them in our guide to the best screenshot API.
The trade-off is real and worth stating plainly: a URL-only API cannot log into a site behind your session or click through an authenticated flow, and Puppeteer can. Keep Puppeteer for journeys that need a real login and interaction, including taking the shot in Puppeteer itself once you are past the gate. Reach for the API when the page is public and the picture is the point.
Common questions about puppeteer stealth and bot detection
Is navigator.webdriver still used to detect bots?
Yes, navigator.webdriver is still one of the first things an anti-bot script reads, because by spec it returns true when the browser is under automation. Puppeteer stealth sets it back to undefined so a basic check passes. On its own that is no longer enough, since detectors combine it with dozens of other signals, but leaving it exposed is an instant giveaway.
Does puppeteer-extra-plugin-stealth work against Cloudflare?
Sometimes, and unreliably. The stealth plugin can clear basic JavaScript checks, but Cloudflare's managed challenges also weigh your TLS fingerprint, IP reputation, and behavior, none of which the plugin changes. From a datacenter IP you will often still get the challenge page, which is why a dedicated rendering service tends to do better here.
How do I check if my headless browser is detectable?
Point your automated browser at a public bot-detection test page and read what it reports about navigator.webdriver, your user agent, plugins, and WebGL. Open the same page in your real browser and compare; the fields that differ are your tells. Run the test from the same IP and machine your production job uses, because IP reputation is part of the score.
Is undetected-chromedriver better than puppeteer stealth?
They solve the same problem for different stacks, so neither is strictly better. undetected-chromedriver patches the ChromeDriver binary that Selenium uses and removes Selenium-specific variables, while puppeteer stealth patches a Puppeteer browser. Pick the one that matches your automation library, and expect the same ceiling from both: they handle JavaScript tells, not TLS or IP.
Does running headful instead of headless avoid detection?
Running a real, non-headless browser removes the headless-specific tells, so it helps, but it does not make you undetectable. A headful browser on a datacenter IP with an automation flag set and no human behavior is still easy to score as a bot. Headful plus stealth plus a residential IP plus human-like behavior is the full stack, and at that point you are maintaining a lot of moving parts.
The honest takeaway: puppeteer stealth is the right first move and it genuinely clears the JavaScript-level checks, so use it when you control the automation and the target is not heavily defended. When the same script keeps tripping Cloudflare from a server, the missing signals are TLS, IP, and behavior, not another patch. And when the screenshot itself is the goal, one HTTP call hands you the clean image without a stealth stack to babysit.



