Playwright CAPTCHA: Tested Libraries and 1,200 Provider Requests

Replaced the old mixed-framework examples, invented solver endpoint and unsupported bypass claims with tested integrations and a reproducible evidence packet. The comparison contains 100 requests per provider/CAPTCHA pair, across six cases and two funded services. Playwright has no built-in stealth mode, and a returned CAPTCHA token is not proof that a protected submission succeeded.
If a CAPTCHA stops your Playwright script, first decide what you need to test. For your own application's automated tests, use provider test keys and verify the acceptance and rejection paths. For a live integration, record both the solver's response and the target server's decision. Clicking a checkbox, receiving HTTP 200, or obtaining a token is not enough.
This guide compares five library approaches and shows how to run the tested examples. The paid comparison contains 1,200 provider requests, including the original 120 observations and 1,080 additional requests. All paid failures remain in the results. The separate introductory four-task experiment is excluded from that denominator.
A CAPTCHA token is an intermediate result
The measured workflow has four stages: load the fixture, obtain a solution, submit it, and check the server's answer. Record which stage failed so a browser setup problem does not become a solver failure, and a token does not become a claimed successful submission.
Our plain Playwright checkbox controls opened Google's public reCAPTCHA demo in one headed and one headless browser. Both encountered image challenges; neither produced a token or an accepted submission. See the headless capture and headed capture.
The invalid-token control demonstrates why checking only the HTTP status is insufficient. These are selected fields from its captured output:
{
"submission_status": 200,
"server_accepted": false,
"success_message_present": false,
"failure_message_present": true,
"assertion_passed": true
}
The demo returned HTTP 200 and rejected the submission. Later, Anti-Captcha returned a token in one paid v2 trial, but the same demo rejected it. That request remains a failure in the comparison below.
Browser diagnostics answer a different question. The Playwright stealth comparison measures observable browser signals; passing those checks does not establish CAPTCHA acceptance.
Install the tested examples and run without paid credentials
Use the immutable example directory at commit 17cc17294d21b31a957f42ff75b6dcac8b6fbb1b. Download or check out that revision, then open examples/bypass-captcha-playwright. Run the commands from its README:
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt
python -m playwright install chromium
npm ci
./run.sh
The measured environment used Python 3.12.11, Node 22.20.0, Playwright 1.63.0 and Chromium 153.0.8010.12 on macOS 26.6.2 arm64. Default checks also passed in Linux CI. On Linux, the packet documents python -m playwright install --with-deps chromium for browser system dependencies. Windows was not tested; the live runners use POSIX signals.
The default command creates no paid tasks, even if solver keys are present. Its recorded checks passed 21 offline tests, five test-key fixture paths and three SDK preflights. Output goes to run_output/; the dated captures stay in expected_output/. Running the checks also regenerates screenshots.
The five fixture paths cover Google test-key acceptance, a local missing-token rejection, and Turnstile test-key acceptance, forced rejection and forced duplicate-token error. The fixture's backend calls the real verification endpoints when appropriate; the missing-token case is rejected locally. These are integration checks, not production CAPTCHA solves. Google's test secret is deliberately permissive, and the Turnstile duplicate error uses a documented dummy secret rather than replaying a production token.
For an application you control, start with the providers' reCAPTCHA testing guidance or Turnstile testing guidance. The packet includes the complete local fixture server.
For browser setup and navigation basics, see Web Scraping with Playwright.
Which Playwright CAPTCHA libraries were tested?
Versions below identify the measured packages, not a claim that they will remain the latest releases.
| Approach | Tested version | What was exercised | What the evidence supports |
|---|---|---|---|
2captcha-python | 2.1.1 | Synchronous official client, six paid cases, target verification where available | A measured Python integration candidate; results below |
anticaptchaofficial | 1.0.70 | Synchronous official client, the same six cases and verification criteria | A second measured integration candidate, with retained failures |
playwright-recaptcha | 0.5.1 | Three bounded audio workflows on Google's public demo | No accepted submission in those workflows |
playwright-extra + puppeteer-extra-plugin-recaptcha | 4.3.6 + 3.6.8 | Widget discovery and the missing-provider path | One widget found; paid solving was not tested through this plugin |
capsolver | 1.0.7 | Installation, import, method signature and credential preflight | Live results unavailable because no key was obtained |
The package inventory, repository snapshots and initial library observations retain the source and execution details. In particular, the Python provider results do not validate a different Node plugin's solving path.
Run one funded Python integration
After the default checks pass, supply TWOCAPTCHA_API_KEY or ANTICAPTCHA_API_KEY through your environment or secret manager. Each command below can spend funds on at most one task. Choose the provider you want to exercise; these commands are separate runs, and rerunning one can incur another charge.
python -c 'import subprocess,sys; sys.exit(subprocess.run([sys.executable,"04_2captcha.py","--live","--trial","1"],timeout=400).returncode)'
For Anti-Captcha:
python -c 'import subprocess,sys; sys.exit(subprocess.run([sys.executable,"05_anticaptcha.py","--live","--trial","1"],timeout=400).returncode)'
These are the bounded commands used for the introductory Google-v2 experiment. The complete shared runner checks the fixture URL/sitekey, obtains a proxyless token, sets the demo response field, submits the form and reads the new document. It requires the expected page, HTTP 200 and Verification Success. Both providers had two accepted submissions in that separate four-task experiment.
The six-case runner uses the same separation between acquisition and acceptance. These selected fields from its first 2Captcha v2 record show what an accepted result records:
{
"outcome": "accepted",
"task_create_requests": 1,
"paid_tasks_created": 1,
"solution_delivered": true,
"server_acceptance_tested": true,
"server_accepted": true
}
The complete provider adapters and fixture/verification functions are the code to inspect when adapting this flow. Their selectors and acceptance checks belong to the named demos; they are not a universal form integration.
The pinned SDKs omit HTTP request timeouts. The measured runners add scoped 10-second connect and 30-second read timeouts. An external 400-second child-process timeout supplements the intended signal deadline. Stopping a client does not cancel a task already accepted by a provider. Error captures keep classes/codes rather than raw exception messages or solutions.
What happened with the audio helper and Node plugin?
The audio helper needs FFmpeg and sends speech transcription to Google's speech-recognition service; it is not a purely local solver. With that prerequisite available, invoke it from the installed packet:
python 02_playwright_recaptcha.py --attempts 1
The recorded headless attempt hit RecaptchaRateLimitError. A headed one-attempt run did not finish the solve. A later headed run with the default five-attempt budget was rate-limited, and testing stopped. Those unequal attempt budgets and a shared host/IP do not support a headed-versus-headless success-rate comparison. See the three captures.
For the Node discovery check:
node 03_extra_recaptcha.cjs
It found one reCAPTCHA widget and returned Please provide a solution provider to the plugin. with zero solutions. That was the expected missing-provider result, not a solved CAPTCHA. Use this evidence to evaluate its discovery plumbing; a funded plugin integration still needs its own test.
CapSolver remains a preflight-only candidate in this packet. Missing credentials are not a failed solve, and no live accuracy, latency or cost is assigned to it.
Results: 100 requests per provider and CAPTCHA case
| CAPTCHA case | 2Captcha | Anti-Captcha |
|---|---|---|
| reCAPTCHA v2 checkbox | 100/100 accepted | 99/100 accepted |
| Invisible reCAPTCHA v2 | 100/100 accepted | 100/100 accepted |
| reCAPTCHA v3 | 100/100 accepted | 100/100 accepted |
| GeeTest v4 | 99/100 accepted | 86/100 accepted |
| Synthetic image OCR | 100/100 correct | 100/100 correct |
| Turnstile | 100/100 tokens; unverified | 100/100 tokens; unverified |
The full comparison and machine-readable summary contain every denominator and failure. The outcome criteria differ:
- v2 and invisible v2: accepted form submission on Google's demos.
- v3: a browser-context request to Google's demo verifier, requiring boolean success, the expected hostname/action, and score at least 0.5. All 200 measured scores were 0.9. This was not a full form-click workflow.
- GeeTest v4: a browser-context request to the 2Captcha-hosted demo backend, requiring its success result. The fixture is hosted by one of the compared vendors.
- OCR: case-insensitive exact matches on 100 distinct synthetic images. Both providers received the same image bytes for each paired trial. These images are not a representative production CAPTCHA dataset.
- Turnstile: token delivery only. The selected public fixture had no backend verifier, so server acceptance is unknown.
Do not combine these into one overall success percentage. The failures were one 2Captcha GeeTest ERROR_CAPTCHA_UNSOLVABLE, thirteen Anti-Captcha GeeTest ERROR_TOKEN_EXPIRED errors, one Anti-Captcha GeeTest ERROR_CAPTCHA_UNSOLVABLE, and one rejected Anti-Captcha v2 submission. None was replaced with another paid attempt.
A separate GeeTest fixture-setup timeout occurred before any provider request. Its original diagnostic was archived after verifying zero creation requests, zero tasks and no charge or ambiguity. That unmeasured slot was then run serially and accepted. The final dataset contains exactly 1,200 requests and confirmed tasks, plus that one uncharged setup diagnostic.
Recommendation from this experiment: start with 2Captcha when reproducing the measured GeeTest integration, while keeping the vendor-hosted-fixture limitation visible. Both official Python clients are candidates for the other server-verified demos. Validate your own target's acceptance separately before making a production choice. For owned-application automated tests, use test keys rather than this paid matrix.
Latency and cost
These are SDK call times for returned solutions, including task creation and polling. The columns show median / p95 seconds; the rejected Anti-Captcha v2 token remains in its timing sample. GeeTest has 99 returned solutions for 2Captcha and 86 for Anti-Captcha; every other cell has 100. Failed provider-call durations are reported separately in the full evidence.
| CAPTCHA case | 2Captcha: median / p95 seconds | Anti-Captcha: median / p95 seconds |
|---|---|---|
| reCAPTCHA v2 checkbox | 6.13 / 61.24 | 24.91 / 154.61 |
| Invisible reCAPTCHA v2 | 2.13 / 62.74 | 34.23 / 139.56 |
| reCAPTCHA v3 | 12.06 / 24.33 | 17.50 / 29.12 |
| GeeTest v4 | 11.84 / 34.87 | 24.24 / 49.00 |
| Synthetic image OCR | 10.52 / 12.04 | 8.09 / 13.99 |
| Turnstile | 10.94 / 16.65 | 16.88 / 23.72 |
The median averages the middle two values for even sample sizes; p95 uses nearest rank. The first ten trials per cell ran with up to four jobs, two per provider. The additional 90 used up to twelve jobs, six per provider, in a later time window on the same submitting host/network; the setup recovery used one job. Provider networks and SDK polling also differ. Pooled timings therefore do not isolate an inherent provider speed difference. The evidence includes separate phase summaries, p90 values and ten-trial blocks.
| Provider | Known receipt subtotal | Receipt coverage | Observed account decrease |
|---|---|---|---|
| 2Captcha | $1.43801 | 599/600 requests | $1.43801 |
| Anti-Captcha | $0.96480 | 586/600 requests | $0.96260 |
Known receipts total $2.40281, covering 1,185 of the 1,200 requests. Observed account decreases total $2.40061. Missing receipts are unknown, not zero-cost tasks. The $0.00220 Anti-Captcha difference remains unreconciled; settlement timing, refunds or other account activity were not established as its cause. These are recorded experiment charges, not a current price quote.
Reproduce the comparison without accidentally repeating paid jobs
Inspect the plan and rebuild the committed results without creating tasks:
python broad_matrix.py --rounds 100
python summarize_100.py --output run_output/rebuilt.json
The first command is an offline plan. The second reads the committed completed dataset. To run a new funded matrix, follow the explicit live-run instructions. A fresh 100-round output directory can create up to 1,200 paid requests; it does not automatically reuse the published observations.
Use one controller per output directory. The matrix skips existing job records, including failures and uncertain creations. Each trial ID allows at most one creation request. Do not delete a failed record to obtain a more favorable result. The narrow setup-recovery tool only releases a slot with proof that no request was sent, preserving its diagnostic first; it is not a paid retry mechanism.
Inspect both outcome and the relevant acceptance field. A provider error, a rejected submission, an OCR mismatch and a token-only observation mean different things. Default CI checks cover test keys and offline assertions; they do not rerun this funded experiment.
Where ScrapingAnt fits—and where it is unnecessary
ScrapingAnt is unnecessary for the local test-key fixtures and direct Playwright/provider integrations above. They run without a ScrapingAnt key.
A separate ScrapingAnt browser/datacenter fetch of Google's demo returned API HTTP 200, target HTTP 200 and widget markup. The recorded cost was 10 credits; no response textarea or success message was found, and no protected form was submitted. That observation does not establish automatic CAPTCHA solving.
The verified credit rule is: A request with JavaScript rendering through a datacenter proxy costs 10 API credits. For product documentation, see CAPTCHA and Cloudflare. This guide makes no broader automatic-solving claim from the single fetch.
Limits to carry into your own integration
This is a dated comparison on named public demos and synthetic images, from one submitting host/network. Fresh browser contexts do not create independent network samples. The providers controlled their own proxyless solver environments. No production-site reliability estimate follows from the counts.
The negative controls are retained in the packet. Immediate v3 replay probes were accepted by Google's demo, while the GeeTest demo rejected both replay probes. These fixture-specific observations do not establish production single-use behavior. Test-key paths intentionally provide controlled outcomes; they are not part of the paid totals.
Keep the stages separate in your implementation: detect the challenge, obtain a solution, submit through the target workflow, and record acceptance or rejection. The useful result is the target's decision, together with enough failure information to reproduce what happened.
For related integrations, see Playwright browser diagnostics and the Puppeteer CAPTCHA guide.
Examples tested on 2026-09-23 with Playwright 1.63.0, 2captcha-python 2.1.1 and anticaptchaofficial 1.0.70. Code: pinned evidence packet.
This article was drafted with AI assistance from a tested evidence packet. The named author is responsible for the code, measurements and corrections.