-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
-
I've been stuck on this for a while and wanted to document it properly in case others run into the same thing :)
My Setup
I'm using CloakBrowser (persistent context) with login to a portal that uses Cloudflare Turnstile. The backend is Django with Celery workers handling the browser automation tasks.
What works
A standalone asyncio script that launches a persistent browser context, opens a new page, and navigates to the portal. Turnstile auto-passes every time, login succeeds, no manual interaction needed. Works with both
headless=Trueandheadless=False.
browser = await launch_persistent_context_async( headless=True, user_data_dir=".user/", ) page = await browser.new_page() await page.goto("https://portal.com") await page.wait_for_timeout(4000)
What does not work
The same portal, same binary, same machine, same profile directory, inside a Celery task. Turnstile consistently shows the checkbox and waits for human interaction instead of auto-passing.
The browser session is managed through a registry that creates oneBrowserContextper company and reuses it across tasks. Pages are created on demand per task.
class BrowserRegistry: _instances: Dict[int, BrowserSession] = {} @classmethod async def get(cls, company_id: int): if company_id not in cls._instances: session = BrowserSession(company_id) await session.start() cls._instances[company_id] = session return cls._instances[company_id]
The Celery task calls into this registry, gets a page, runs the login flow. Everything is structurally identical to the standalone script except the execution context.
What the logs show
On the very first task after a fresh Celery startup, no prior sessions, no reuse happening yet:
[SESSION] STARTING company=1 profile=.../profiles/company_1
[SESSION] READY startup=1.25s initial_pages=1
[PAGE] REUSING slot=0 url='about:blank'
[AUTH] NAVIGATING to base_url
# 30 seconds pass...
TimeoutError: wait_for_load_state("networkidle") timed out
[PAGE CLOSE] url='https://portal.com/account/login?returnUrl=...'
The page navigates to the portal, gets redirected to login, and then hangs. The timeout is because Turnstile is showing the checkbox and keeping the page in an active network state waiting for interaction. If you manually click the checkbox it passes and login works fine, so Turnstile is not hard-blocking, just not auto-passing.
I have tried
- Not a headless detection issue, standalone works in both modes
- Not a session reuse issue, fails on the very first task after fresh startup
- Not an event loop issue, moved from asyncio.run() per task to a persistent loop
- Not a profile/cookie issue, same profile directory used in both cases
- Not a network issue, same machine, same connection
Has anyone seen Turnstile scoring behave differently based on how the browser process was spawned or what process context it runs in? Is there something CloakBrowser sets up differently when launched from an interactive terminal vs a background worker process that could affect how Turnstile evaluates the session?
Any insight into what signals Turnstile might be picking up differently between these two environments would be really helpful.
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment 1 reply
-
Thanks for the detailed write-up.
The most likely cause is accumulated CDP (Chrome DevTools Protocol) traffic in the long-lived Celery browser session.
Two things to change first:
-
Replace
page.wait_for_timeout()withawait asyncio.sleep()—wait_for_timeout()sends CDP commands that Turnstile can detect.asyncio.sleep()is invisible to the browser. Same forpage.wait_for_load_state("networkidle")— it opens a CDP listener that monitors all network activity. Useasyncio.sleep()with a fixed delay instead. -
Create a fresh page instead of reusing the default
about:blank— your logs show[PAGE] REUSING slot=0 url='about:blank'. Useawait browser.new_page()and close the page when done. The default page from persistent context has been alive since browser launch, accumulating CDP state.
If Turnstile still shows the checkbox after those changes, use the keyboard approach instead of clicking it:
await asyncio.sleep(3) # let Turnstile widget initialize await page.keyboard.press("Tab") await asyncio.sleep(0.5) await page.keyboard.press("Space")
This has been confirmed working by multiple users in Docker and headless environments (#53, #79). Keyboard events don't go through the CDP mouse dispatch path that Turnstile detects.
The core difference between your standalone script and Celery is that the standalone script launches, navigates immediately, and exits — minimal CDP footprint. The Celery worker keeps the browser alive across tasks, and the idle CDP connection generates background protocol traffic that degrades Turnstile's trust score.
Let us know if that helps.
Beta Was this translation helpful? Give feedback.
All reactions
-
I’ve switched from using launch_persistent_context to connecting via a CDP connection instead, and I’m now running the browser through a separate browser-pool microservice. It seems to be working better so far, though I’m still validating.
Thank you for the keyboard approach!!
Beta Was this translation helpful? Give feedback.