Our own stealth browser
TL;DR
- On Fidelity (Akamai) and Zillow (PerimeterX), our stealth browser Kestrel loaded 12 of 12 pages; stock headless Chromium loaded 0 of 12, on the same proxy exits.
- Kestrel builds one consistent identity (user agent, client hints,
navigator, GPU) and takes its time zone and language from the proxy exit. - Stock headless leaked our server’s real IP through WebRTC; Kestrel shows a page only the proxy.
- We drive it over raw Chrome DevTools Protocol, without the
Runtime.enablecall most automation libraries leave behind. - Limits, measured: where a site blocks the IP (Redfin, Google), a better browser changes nothing, and neither browser gets past an interactive Cloudflare check.
Our agents browse real websites for real people. When a stock headless browser hits a bot wall, the agent fails, and the person gets no answer. So we built our own stealth browser, Kestrel, and a bench that measures it against stock headless Chromium on the same proxy exits.
The results, on live sites, 2026-10-06:
| Stock headless Chromium | Kestrel | |
|---|---|---|
| Fidelity (Akamai), 3 pages × 3 exits | 0 / 9 | 9 / 9 |
| Zillow (PerimeterX), 3 exits | 0 / 3 | 3 / 3 |
| CreepJS “like headless” | 94% | 31% |
| BrowserScan authenticity | 75% | 90% |
| bot.sannysoft.com | 3 FAIL, 2 WARN | no FAIL, no WARN |
| WebRTC | leaks the server’s real IP | proxy only |
This post is about the engineering: what a browser has to get right to be believed, how Kestrel is put together, how we drive it, and how we measured it. Kestrel is ours and is not available online. We describe its design, not its origins.
The problem: a browser that tells two stories
The usual fix for a blocked headless browser is a fake User-Agent. Our research
agent did exactly that: a Windows Chrome 138 UA, the AutomationControlled blink
feature off, and --lang=en-US.3 It was added for a good reason. With
the default HeadlessChrome UA, cnn.com served 164 bytes and zap.co.il answered
“Request Rejected”.
But a UA flag changes one string. Chrome also sends UA client hints
(Sec-CH-UA, Sec-CH-UA-Platform) and exposes them to scripts through
navigator.userAgentData.4 Those come from the real binary, and
Playwright’s headless mode runs a separate headless-shell build, Chromium 140 in
our install.567 So the patched browser said this
about itself, on a US exit:
| Signal | Stock headless, UA patched | Kestrel |
|---|---|---|
User-Agent header |
Chrome/138, Windows | Chrome/152, Windows |
Sec-CH-UA brands |
Chromium 140, HeadlessChrome 140 | Chromium 152, Google Chrome 152 |
Sec-CH-UA-Platform |
“Linux” | “Windows” |
Accept-Language header |
none sent | en-US,en;q=0.9 |
navigator.platform |
Linux x86_64 | Win32 |
navigator.languages |
en-US@posix |
en-US, en |
| Time zone | UTC (on a US exit) | America/New_York |
| CPU cores reported | 96 | 8 |
| WebGL renderer | SwiftShader (software) | a desktop GPU, Direct3D 11 |
Source: httpbin.org/headers and an in-page probe, US exit, try 1.1
Every bold cell contradicts the first row. The patched browser claims Windows
and then reports Linux three times. It claims Chrome and names itself
HeadlessChrome. It sends no Accept-Language at all, which real Chrome always
does.8 The last three rows are not lies, but they describe a server, not
a desktop in New York. A bot wall needs only one contradiction.
That table is the design brief. Patching signals one at a time does not work, because every patch is a new place for two signals to disagree.
How Kestrel is put together
Kestrel has four layers. Each one exists because of a row in that table.
1. One identity, generated as a whole
Kestrel does not randomise values one by one. It builds a single, consistent
profile and derives every surface from it: the UA string, the client-hint
brands and platform, the navigator fields, the screen, the GPU and the
locale. A profile that says “Chrome 152 on Windows” says it in the
header, in the hints, in navigator.platform, in userAgentData and in the
WebGL renderer, because all of them read from the same source.
The engine reports itself as Chrome 152.0.7977.64, and 152 is the version in every string, so the version a page reads matches the version its JavaScript runs on.7
2. The network decides the locale
A Windows desktop on a New Jersey exit with a UTC clock and no language header is
not a person. Kestrel takes the time zone and the language from the proxy exit
it is started with. On our first smoke test the exit was in Spain, and Kestrel
chose es-ES and Europe/Madrid without being told to.9 On the bench we
asked for en-US explicitly, and the Virginia exit gave
America/New_York.1
3. WebRTC cannot go around the proxy
A proxy hides the address a page connects from. It does not hide the address WebRTC reports. In stock headless, the STUN candidate on CreepJS, BrowserScan and Pixelscan was our server’s own public IP, in a different country from the proxy exit, shown on the same page as the proxy’s IP.2 A “proxied” session was telling the site where it really ran. In Kestrel, non-proxied WebRTC is blocked, and the pages saw only the proxy.2
4. A service, a fresh profile per session, and a raw-CDP driver
Kestrel runs as a service. A client creates a profile (an OS family and the languages it wants), starts that profile with a proxy, and gets back a Chrome DevTools Protocol endpoint. When the session ends, the profile is deleted. Nothing carries over from one session to the next: no cookies, no storage, no history.10
We drive it over raw CDP, not through Playwright. That is a deliberate choice, for two reasons:
- Playwright’s high-level calls hang on it. Page title, screenshots and selectors never return. Plain navigation and evaluation work.
- Automation libraries leave their own fingerprint. Most of them call
Runtime.enableon every page so they can track execution contexts, and that call is a widely discussed automation tell.11 Our driver never calls it.
The driver is small. It attaches to the page target in flattened mode
(Target.attachToTarget with flatten), enables the Page domain, enables
Network only to block media with Network.setBlockedURLs, and runs its probes
with Runtime.evaluate, which works without Runtime.enable.10 Screenshots come straight from
Page.captureScreenshot.
The cost is integration. Our eval harness is built on Playwright, so moving a workload to Kestrel is a driver change, not a flag.
The bench
A stealth claim without a controlled comparison is marketing. The bench is built so that only one thing differs between the two arms: the browser.
Same exit, both browsers. Each try uses one datacenter exit from our basic proxy system, and both browsers use that exit. We used three US exits, A, B and C, in Virginia, Texas and New Jersey.7 This is what lets us tell a browser block from an IP block (see “What the IP decides”).
Success is decided before the run. Each site has a predicate in the bench’s judge function, written before any result was seen:
| Site | URL | “ok” means | “blocked” means |
|---|---|---|---|
| Zillow | /seattle-wa/ |
a result count (“N homes”), and more than 2,000 characters of text | “Press & Hold” / px-captcha |
| Redfin | /city/16163/WA/Seattle |
at least 5 prices like $ddd,ddd |
“automated” / “access denied”, or a 405 page |
search?q=weather+in+paris |
a result heading (#search a h3) |
/sorry or “unusual traffic” (429) |
|
| Fidelity ×3 | screener (www), a fund page, screener (fundresearch) |
no “Access Denied”, more than 1,500 characters, and “fidelity” in the text | Access Denied, a stall, or a protocol error |
/company/microsoft/ |
the path stays /company/, h1 contains “Microsoft”, more than 1,500 characters |
authwall, or status 999 | |
| Turnstile (real key) | peet.ws non-interactive test | a cf-turnstile-response token longer than 20 characters |
no token in 60 s |
| CF challenge | nopecha.com Cloudflare demo | the interstitial is gone and the page text is shown | still on “Just a moment…” |
Source: the bench’s success predicates, fixed before the first run.10
Timing starts just before navigation and stops when the predicate first passes, polled every 500 ms. A page that never passes is a failure at 60 s. At most 3 tries per cell, one per exit.10
Small on purpose. Fewer than 150 fetches in total, $0.00 of model spend (no model is called), proxy bandwidth only.7
Results
| Site | Stock headless | Kestrel |
|---|---|---|
| Zillow | 0/3 (403, Press & Hold) | 3/3, median 4.6 s |
Fidelity screener (www) |
0/3 (HTTP/2 protocol errors, a network error) | 3/3, median 6.8 s |
| Fidelity fund page | 0/3 (timeout, empty response) | 3/3, median 3.8 s |
Fidelity screener (fundresearch) |
0/3 (timeout, empty responses) | 3/3, median 2.9 s |
| LinkedIn company page | 3/3, median 2.5 s | 3/3, median 2.2 s |
| Redfin | 1/3 † | 1/3 |
| Google search | 1/3 | 1/3 |
| Turnstile, real key | 3/3, median 3.7 s | 3/3, median 5.1 s |
| Cloudflare challenge | 0/3 | 0/3 |
Source: 54 matrix loads, 12 challenge loads and 2 redo loads.11213
† On exit A, stock headless’s first Redfin and Google tries died with a network
error on our side (ERR_NETWORK_CHANGED), not a site block. We re-ran those two
cells once on exit A. Redfin then passed in 2.6 s, and Google got a 429.13
The same error also hit two of stock headless’s nine Fidelity tries (both on
exit C), which we did not re-run. The other seven failed with Akamai-style
protocol errors and stalls. A bare Playwright launch with no flags at all
(navigator.webdriver true) was blocked on Zillow and timed out on the fund page
too, in a single try.114
Akamai tends to stall a suspect client rather than serve a block page. We saw
ERR_HTTP2_PROTOCOL_ERROR, ERR_EMPTY_RESPONSE and plain timeouts, and there
was never a page to read. Kestrel, on the same exits, loaded the FBALX fund page
(the Fidelity Balanced Fund) every time.1
This started with one eval run. An agent asked about Fidelity mutual funds, and our headless browser timed out at 60 s on both Fidelity pages it tried. On the same proxy type, Kestrel loaded the screener in 20.6 s and the fund page, 7.8k characters of it, in 9.9 s. The matrix above is that anecdote turned into a measurement.
What the IP decides
Redfin and Google did not care which browser we used:
| Site | Exit A | Exit B | Exit C |
|---|---|---|---|
| Redfin | ok for both | 405 “Human Verification” for both | 405 for both |
429 /sorry for both |
ok for both | 429 for both |
Sometimes it is the IP. These datacenter exits are on Redfin’s and Google’s lists in a pattern of their own, and a better browser changes nothing there. The bench can say so only because both browsers shared each exit. With a different exit per browser, Redfin would have looked like a browser result.
On Zillow and Fidelity the exit was held fixed and the browser decided the outcome. Kestrel differs from stock headless in many signals at once, so the matrix does not say which single signal each site keys on. It says that a browser whose signals agree got in, on the same exits where one whose signals disagree did not.
What the “is it a bot?” pages say
Public fingerprint pages are a quick, free second opinion. We loaded ten of them in both browsers on 2026-10-08, on the same Virginia exit, one load each.15 One pair tells the story; the table below has the rest.
A verdict in one line. deviceandbrowserinfo.com calls stock headless a bot
and Kestrel a human. Its raw details name why: for stock headless,
isAutomatedWithCDP, hasInconsistentChromeObject and
hasHighHardwareConcurrency are all true. For Kestrel every check is
false.15 The first of those points at the automation driver, not the profile, which is why
Kestrel is driven over raw CDP.

The WebRTC leak. BrowserScan’s IP panel, same exit. Stock headless shows the proxy in the USA, and then, through WebRTC and STUN, our server’s own addresses, located in Germany. Kestrel reports WebRTC as disabled, so the only address the page learns is the proxy’s.15
The classic checklist. bot.sannysoft.com’s first table. Stock headless fails
five rows: no window.chrome, the permissions check, zero plugins, a plugin
list of the wrong type, and a SwiftShader software renderer. Kestrel passes all
eleven, with 5 plugins and a desktop GPU under Direct3D 11.15
The strictest page. CreepJS rates stock headless 94% “like headless” and Kestrel 31%. The same view shows why the locale layer exists: stock headless is on UTC behind a US exit, and Kestrel is on Eastern Daylight Time.15
The pages do not agree with each other, and Kestrel does not pass all of them:
| Page | Stock headless | Kestrel |
|---|---|---|
| deviceandbrowserinfo.com | “You are a bot!” | “You are human!” |
| bot.sannysoft.com, first table | 5 red rows | all green |
| CreepJS “like headless” | 94% | 31% |
| BrowserScan authenticity | 75%, proxy “Yes” | 90%, proxy “No” |
| rebrowser bot detector | 2 red (default viewport, no Chrome brand in userAgentData) |
all green |
| Pixelscan | “inconsistent”: time zone spoofed, proxy, masking, automation | “inconsistent”: proxy detected; no automation detected |
| BrowserScan bot detection | “Normal” | “Normal” |
Source: 2026-10-08 fingerprint run.15
Pixelscan still calls Kestrel inconsistent, because it recognises the datacenter exit as a proxy. That is the IP, not the browser, the same split as Redfin and Google above. BrowserScan’s dedicated bot page passes both browsers, so a single green page proves little. BrowserScan’s own deductions for Kestrel were a WebGL exception and an audio exception, 5% each.15 Those are what the fingerprint-noise work below is for.
A lesson about our own test: the Turnstile demo
Our first matrix included Cloudflare’s Turnstile demo at
demo.turnstile.workers.dev. Both browsers “passed” it, stock headless
included.1 That demo uses a Cloudflare test sitekey, which always
passes. It measures nothing. We replaced it with a page that uses a real sitekey
in non-interactive mode. Both browsers still got a token, on all three
exits.12
The nopecha.com Cloudflare demo showed the interactive “Verify you are human” checkbox to both browsers on all three exits: 6 cells, 6 × 403 and the checkbox.12 Our bench does not click checkboxes or solve challenges, on purpose (see “Ethics”). This row says that neither browser skips the interactive step without a click, and nothing about what happens after one.
How to bypass Cloudflare Turnstile with ease
Sorry, we can’t write all the details here.
However, we did extensive work on Cloudflare and other anti-bot systems, and we really do mean extensive.
What is next
- Kestrel in our research agent. That needs a CDP driver for the agent’s harness first, and then a rerun of the Fidelity task that started this.
- Residential and mobile exits. Every exit here was a datacenter address from our basic proxy system. On an advanced proxy system (residential exits), Redfin and Google might not split by exit.
- Fingerprint noise. Per-site noise options, in the style of Brave’s randomisation, are in progress and were not part of this run.
- WebRTC in our headless workloads. Turn off non-proxied WebRTC there too.
The proxy is chosen per site, separately from the browser. Kestrel pairs with the basic proxy system by default, and government sites always take a separate, dedicated route.
Ethics
We built Kestrel for research and agent reliability. Our agents act on behalf of real users, who ask them to read pages a person could read in their own browser. A browser that is consistent about being Chrome is part of that.
What we did in this bench, and what we do not do:
- We loaded public pages only. No accounts, no logins, no paywalls bypassed.
- We solved no CAPTCHAs and clicked no challenge. Where a site asked for an interaction, we recorded that as the result.
- We kept volume small: fewer than 150 fetches, at most 3 tries per page, and a 60 s ceiling.7
- Every live fetch went through a proxy provider, not our servers’ own IPs. Government sites take a separate, dedicated route.
- We do not use Kestrel for account farming, fraud, ad fraud, scalping, or getting around a ban placed on a user.
Nothing in this post says that any site’s terms of service allow automated access. Whether they do is a question for each site and each use.
Footnotes
-
54 loads (9 sites × 2 browsers × 3 tries), plus a screenshot per cell. Medians are over the passing tries. Fidelity fund page title: “FBALX - Fidelity® Balanced Fund | Fidelity Investments”. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
8 loads on 2026-10-06, exit A, a 25 s wait for CreepJS and 18 s for the others. Pages: https://abrahamjuliot.github.io/creepjs/, https://www.browserscan.net/, https://pixelscan.net/fingerprint-check, https://bot.sannysoft.com/. ↩ ↩2 ↩3
-
Stock headless launch flags, as our research agent used them:
--disable-blink-features=AutomationControlled,--user-agent=Windows Chrome/138.0.0.0,--lang=en-US. ↩ -
WICG, User-Agent Client Hints. https://wicg.github.io/ua-client-hints/ ↩
-
Playwright docs, “Browsers”: headless mode uses
chromium-headless-shell. https://playwright.dev/docs/browsers ↩ -
Chrome for Developers, “Chrome Headless mode”. https://developer.chrome.com/docs/chromium/headless ↩
-
Measured 2026-10-06, $0.00 model spend (no model was called), proxy bandwidth only. The loads reported in this post: 54 matrix, 12 challenge, 2 redo, 2 smoke, 3 bare-launch and 8 fingerprint, plus 22 small geolocation lookups to choose US exits and 4 header checks with curl. Fewer than 150 fetches in total. The exits were chosen from our basic proxy system’s datacenter list by geolocation: A Virginia, B Texas, C New Jersey. Node 22, Playwright 1.55, Chromium headless shell 140.0.7339.16. Kestrel reported Chrome 152.0.7977.64. ↩ ↩2 ↩3 ↩4 ↩5
-
Castle blog, on Playwright and headless tells, including the missing
Accept-Languageheader. https://blog.castle.io/ ↩ -
Kestrel’s first smoke test, on a Spanish exit, reported
es-ESand Europe/Madrid without a locale being requested. ↩ -
Stock headless was driven through Playwright with the research agent’s launch flags. Kestrel was driven over raw CDP on a WebSocket (
Target.attachToTargetwithflatten,Page.enable, noRuntime.enable). Both arms block media withNetwork.setBlockedURLsand share one judge function. Kestrel gets a temporary profile per try, deleted afterwards. ↩ ↩2 ↩3 ↩4 -
rebrowser, “How to fix Runtime.Enable CDP detection of Puppeteer, Playwright and other automation libraries”. https://rebrowser.net/blog/how-to-fix-runtime-enable-cdp-detection-of-puppeteer-playwright-and-other-automation-libraries ↩
-
12 loads, both browsers: nopecha.com Cloudflare demo (all 6: 403, “Just a moment…”, checkbox shown) and peet.ws real-sitekey non-interactive Turnstile (all 6: token issued). ↩ ↩2 ↩3
-
Stock headless, exit A, one try each: Redfin ok in 2,648 ms, status 200. Google blocked, 429. ↩ ↩2 ↩3
-
Playwright with no launch flags at all, 3 loads, exit A, one try each. Zillow: “Access to this page has been denied” / Press & Hold. Fidelity fund page: navigation timeout. ↩
-
20 loads on 2026-10-08 (10 pages × 2 browsers), one Virginia datacenter exit for both, a 25 s wait per page and 45 s for Pixelscan and bot.incolumitas.com. Stock headless was Chromium 140 (headless shell) with the research agent’s launch flags. Pages also loaded but not shown: fingerprint.com’s bot-detection demo, iphey.com and bot.incolumitas.com. The screenshot is cropped; no other edits. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7