A Selenium script that runs clean on your laptop meets its first reCAPTCHA somewhere around page forty of a real crawl. From there the arithmetic changes. Buying a token for every page of a ten-thousand-page run costs between $6 and $30 at the prices the four main solver APIs published on 13 August 2026. It also adds three to ten seconds per page, which is eight to twenty-eight hours of wall clock if you run pages one at a time. The interesting engineering question is not how to solve a reCAPTCHA. It is how seldom you can get away with solving one.
The mechanics still matter, because sooner or later you need them: read the site key, buy a token, put that token where the page actually reads it. Step three is where most write-ups go wrong, and the earlier version of this article was one of them. We ran the injection code in a real browser this month, and the measurements below say exactly how it fails.
Everything here was checked against Python 3.11, Selenium 4.47.0 (released 10 August 2026) and 2captcha-python 2.1.0. Prices come from each vendor's own pricing page, read on 13 August 2026. Sister articles cover the same job from other angles: reCAPTCHA v2 and 2Captcha in plain Python, the C# integration, adding reCAPTCHA to a PHP form if you are on the defending side, and a price and coverage comparison of the solver market.
A quick, honest disclaimer: solving CAPTCHAs to automate a site can violate that site's terms of service. Do it on properties you own or are authorized to test, respect
robots.txtand rate limits, and never use these techniques for fraud or account abuse.
What a solve costs before you write a line of code
Prices per 1,000 solves, read from each vendor's own page on 13 August 2026:
| Service | reCAPTCHA v2 | reCAPTCHA v3 | Enterprise | Speed the vendor advertises |
|---|---|---|---|---|
| 2Captcha | $1–$2.99 | $1.45 at score ≤ 0.3, $2.99 above | $1–$2.99 | not readable |
| Anti-Captcha | $0.95–$2 | $1–$2 | $5 | 5 s average |
| CapSolver | $0.80 | $1.00 | $1 for v2, $3 for v3 | under 5 s, under 3 s for v3 |
| CapMonster Cloud | $0.60 | $0.90 | $1 for v2, $1.50 for v3 | not stated |
Two things about that table. 2Captcha's pricing page renders its speed column as a row of zeroes to anything that is not a full browser, so we have no latency figure from them worth quoting. And the ranges are not tiers you choose. They are bid ranges: the low end is what a job costs when the queue is empty and workers are idle, the high end is what it costs when it is not.
Do the arithmetic before you build. Ten thousand pages, one solve each, is $6 at CapMonster's advertised rate and $29.90 at 2Captcha's ceiling. That is small money. The time is not. At Anti-Captcha's advertised five-second average, ten thousand serial solves is thirteen hours and fifty-three minutes of doing nothing but waiting. Run twenty solves concurrently and it drops to forty-two minutes, which is the number that decides whether the crawl fits in a night. One extra second per page across the same run is two hours and forty-seven minutes.
None of that includes the pages where the solve fails and you pay twice, and none of it includes proxy cost, which on a job like this usually exceeds the solver bill.
Which reCAPTCHA is in front of you
Identify the variant first, because it decides what you send and what you get back.
- reCAPTCHA v2, the checkbox. A
div.g-recaptchawith adata-sitekeyattribute. Ticking the box may pass you outright or may escalate to an image grid. - reCAPTCHA v2 invisible. Same engine, no checkbox, fires on a button click or on
grecaptcha.execute(). Google's own invisible reCAPTCHA documentation states that "theg-recaptcha-responsetoken is passed to your callback", which matters later: on an invisible widget the callback is the delivery path, not a nicety. - reCAPTCHA v3. No challenge, no checkbox, no grid. It returns a score and the site decides. Google's v3 documentation is blunt about the scale: "1.0 is very likely a good interaction, 0.0 is very likely a bot", and "by default, you can use a threshold of 0.5".
- reCAPTCHA Enterprise. The page calls
grecaptcha.enterprise.execute(...). One line of JavaScript tells you which you are on:!!(window.grecaptcha && window.grecaptcha.enterprise).
A correction to the earlier version of this article, which called Enterprise "a paid, hardened version". Google's reCAPTCHA FAQ says Enterprise "offers up to 10,000 assessments per month at no cost", and that classic reCAPTCHA is the version with a hard ceiling: "if you want to make more than 1,000 calls per second or 1,000,000 calls per month, you must use reCAPTCHA Enterprise or fill out this form and wait for an exception approval". Enterprise is not the paid tier. It is where Google steers any site with traffic, and the migration "requires no code changes" by Google's own account. Expect to meet it more often each year, not less.
What the widget measures, and how much of that is knowable
Mouse paths, event timing, scroll behavior, keystroke cadence, cookie age and IP reputation all feed the score. Every list of signals in circulation, including the one the earlier version of this article printed as fact, is reverse-engineering plus solver marketing. Google publishes the score and the action name, and nothing underneath them. The honest version is that the raw signals go to Google encrypted, the site never sees them, and the only thing that crosses the boundary is a token.
Why random tile clicking never came back
The 2015-era trick was to click grid tiles at random and resubmit. Numbers get quoted for the odds of a lucky solve, usually two or three percent, and none of them trace to a published measurement. What is documented is simpler and more final: a response token is valid for two minutes, and grids only appear on v2 at all. Invisible v2 and v3 give you nothing to click. Whatever the odds once were, there is no longer a puzzle on most of the pages that stop you.
Solvers are for the residue.
The token is the entire protocol
Everything else is plumbing around one string. Google's verification documentation states the constraint in one sentence: "Each reCAPTCHA user response token is valid for two minutes, and can only be verified once to prevent replay attacks."
The earlier version of this article said the token was "valid for about two minutes" and stopped there. The missing half is what actually bites. Single use means a retry loop that resubmits the same token gets the same rejection forever, and the error code is timeout-or-duplicate, documented as "the response is no longer valid: either is too old or has been used previously". One error string covers both an expired token and a replayed one, so from the outside you cannot tell which mistake you made.
Two more properties decide whether a token works:
- The clock starts when the worker solved it, not when your code received it. A solver that took ninety seconds hands you a token with thirty seconds of life left. The 2captcha-python client polls every ten seconds by default, so up to another ten seconds of that budget is gone before your process sees the string.
siteverifyreturns thehostnamewhere the captcha was solved. Solve against a URL that differs from the one you submit from, even by a redirect, and a site that checks the field rejects a token that looks perfectly valid.
Selenium cannot produce this string. Driving a browser gets you the site key and gets the token onto the page; it does not get you past Google's risk model, because the model is not evaluated in your process. That is the whole reason an external solver exists.
Step 1: read the site key
The site key is public by design. It sits in data-sitekey, and when there is no visible widget it is the k= parameter of the reCAPTCHA iframe URL. This helper covers both and reports whether the page is on Enterprise:
from urllib.parse import urlparse, parse_qs
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
def read_sitekey(driver, timeout=15):
"""Return (sitekey, is_enterprise) for the first reCAPTCHA on the page."""
try:
widget = WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)
key = widget.get_attribute("data-sitekey")
except TimeoutException:
key = None
if not key:
frames = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='recaptcha']")
if not frames:
raise LookupError("no reCAPTCHA widget on this page")
query = parse_qs(urlparse(frames[0].get_attribute("src")).query)
key = query["k"][0]
enterprise = driver.execute_script(
"return !!(window.grecaptcha && window.grecaptcha.enterprise);"
)
return key, bool(enterprise)The selector is [data-sitekey] rather than .g-recaptcha on purpose. Plenty of integrations put the attribute on the submit button instead of a div, and the class name is not guaranteed. find_elements returns an empty list instead of raising, which is why the iframe branch needs no second try.
You do not need to enter the reCAPTCHA iframes for any of this, and driver.switch_to_frame() is not available to you anyway. We checked on Selenium 4.47.0: WebDriver exposes exactly one member matching switch, and it is the switch_to object. Both switch_to_frame and switch_to_default_content are gone, replaced by driver.switch_to.frame(...).
Step 2: buy a token
pip install selenium 2captcha-pythonSelenium ships Selenium Manager with every release "as of version 4.6", so nobody downloads chromedriver by hand any more, and "as of Selenium 4.11.0, Selenium Manager also implements automated browser management" and will fetch the browser too. It is still labeled beta after four years, and it needs outbound network access to Google's Chrome for Testing endpoints. In a locked-down container it fails, and the fix is to pin a driver binary yourself.
The solving call itself is four lines:
from twocaptcha import TwoCaptcha
solver = TwoCaptcha(API_KEY, polling_interval=5)
answer = solver.recaptcha(sitekey=sitekey, url=driver.current_url, enterprise=1)
token = answer["code"]Details worth knowing before this runs unattended, all of them read out of the library's own source rather than the marketing pages:
- The defaults are generous.
default_timeoutis 120 seconds,recaptcha_timeoutis 600,polling_intervalis 10. A stuck reCAPTCHA job blocks your worker for ten minutes before it raises. Dropping the polling interval to 5 halves the average delay between the worker finishing and your process learning about it, and buys back five seconds of the token's two-minute life. NetworkExceptionmeans two different things. Internally the client raises it both when the API answersCAPCHA_NOT_READYand when the HTTP request genuinely fails, andwait_resultswallows both and sleeps. A solver outage, a DNS failure and a slow captcha are indistinguishable untilrecaptcha_timeoutexpires.- Version 2.1.0 still talks to the old API.
ApiClientposts tohttps://2captcha.com/in.phpandres.php, not the newercreateTaskJSON endpoints. Your key travels as a form field, which is fine, but do not assume the library follows the v2 docs you are reading. data-scannot be passed as a keyword. It is a legal parameter name for the HTTP API and an illegal Python identifier. Usesolver.recaptcha(sitekey=..., url=..., **{"data-s": value}).- Setting
callbackmakessolve()returnNone. Configure a pingback URL and the client submits the job and returns without a result, by design. Code that readsanswer["code"]then dies on aTypeErrora long way from the cause. - The exceptions live one level down.
from twocaptcha.exceptions.solver import ApiException, NetworkException, TimeoutException.ApiExceptionis what carriesERROR_ZERO_BALANCE,ERROR_KEY_DOES_NOT_EXISTandERROR_CAPTCHA_UNSOLVABLE, becauseApiClientraises it for any response containing the stringERROR.
Every competing API mirrors this shape closely enough that swapping providers is a base URL and a parameter rename. That is the practical argument for wiring a second provider in early rather than after the first outage.
Step 3: put the token where the page reads it
The token belongs in the hidden textarea named g-recaptcha-response, and then the widget's callback has to fire. Almost every tutorial, this one included until now, writes it like this:
driver.execute_script(
"document.getElementById('g-recaptcha-response').innerHTML = arguments[0];", token
)That line works often enough to look correct and fails silently when it does not.
What we measured
Setup. Chromium 141.0.7390.37 driven by chromedriver 141.0.7390.54 through Selenium 4.47.0 on Python 3.11, --headless=new, against a local HTML file reproducing the DOM shape reCAPTCHA leaves behind: two forms, two hidden textareas both named g-recaptcha-response with ids g-recaptcha-response and g-recaptcha-response-1, one plain data-callback and one written as a dotted path.
- On a field nothing has written to yet,
innerHTMLworks. Set it toTOKEN_A, read.valueback, getTOKEN_A. - Once anything has assigned to
.value,innerHTMLstops working, without an error. We wroteOLD_EXPIREDto.value, then set.innerHTMLtoTOKEN_B. Reading.innerHTMLgaveTOKEN_B. Reading.valuegaveOLD_EXPIRED. The form submits the old string. This is the HTML dirty-value flag: a textarea's value tracks its child text only while nothing has written to the value directly. reCAPTCHA writes to the value itself, so any page where the widget already produced a token, a checkbox that was ticked and expired, or a second attempt after a rejected token, is a page where theinnerHTMLtrick is dead. .valueworks in both states. There is no case whereinnerHTMLsucceeds and.valuedoes not.getElementByIdfinds one field out of two.document.getElementsByName('g-recaptcha-response')returned 2.document.getElementById('g-recaptcha-response')returned the first form's field. On a login page with a second widget in a newsletter box, the token lands in the wrong form and the submit fails for no visible reason. Google's own API acknowledges the plural case:grecaptcha.getResponse(opt_widget_id)takes a widget id, "defaults to the first widget created if unspecified", per the display documentation.window[cb]is undefined for a dotted callback. Withdata-callback="app.onSolveTwo",typeof window["app.onSolveTwo"]isundefined, so the popular one-liner does nothing and reports nothing. Splitting on dots and walkingwindowresolves it, and the callback then fires with the token.- An
execute_scriptround trip cost 2.37 ms on average over 200 calls. Injection is not where your time goes.
Measured on one machine against a local file. The 2.37 ms is a floor, not a promise. The five behavioral results are browser semantics rather than timing and will reproduce anywhere.
Injection that survives all of that
INJECT = """
const token = arguments[0], index = arguments[1];
const fields = document.getElementsByName('g-recaptcha-response');
if (!fields.length) { return 'no-response-field'; }
const field = fields[index] || fields[0];
field.value = token;
const widgets = document.querySelectorAll('.g-recaptcha, [data-sitekey]');
const widget = widgets[index] || widgets[0];
const name = widget && widget.getAttribute('data-callback');
if (!name) { return 'field set, no callback declared'; }
const fn = name.split('.').reduce((o, k) => (o ? o[k] : undefined), window);
if (typeof fn !== 'function') { return 'callback declared but missing: ' + name; }
fn(token);
return 'callback fired: ' + name;
"""
status = driver.execute_script(INJECT, token, 0)It returns a string instead of failing quietly, which is the point. Log that string. When a run starts failing, no-response-field and callback declared but missing tell you the page changed, and they tell you in one line instead of a night of guessing.
The whole script
"""reCAPTCHA v2 through 2Captcha, driven by Selenium.
Checked against Python 3.11, selenium 4.47.0, 2captcha-python 2.1.0.
"""
import os
import time
from urllib.parse import urlparse, parse_qs
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from twocaptcha import TwoCaptcha
from twocaptcha.exceptions.solver import ApiException, NetworkException
from twocaptcha.exceptions.solver import TimeoutException as SolverTimeout
TARGET_URL = "https://example.com/form-with-recaptcha"
TOKEN_BUDGET = 90 # seconds; the token dies at 120
# read_sitekey() from Step 1 and the INJECT string from Step 3 belong here.
def solve_and_submit(driver, solver):
driver.get(TARGET_URL)
sitekey, enterprise = read_sitekey(driver) # from Step 1
page_url = driver.current_url # after redirects
started = time.monotonic()
try:
answer = solver.recaptcha(
sitekey=sitekey,
url=page_url,
enterprise=1 if enterprise else 0,
)
except ApiException as exc: # ERROR_ZERO_BALANCE etc.
raise RuntimeError(f"2captcha refused the job: {exc}") from exc
except (SolverTimeout, NetworkException) as exc:
raise RuntimeError(f"no token within the timeout: {exc}") from exc
age = time.monotonic() - started
if age > TOKEN_BUDGET:
raise RuntimeError(f"token arrived {age:.0f}s old, not worth submitting")
status = driver.execute_script(INJECT, answer["code"], 0) # from Step 3
print(f"solved in {age:.1f}s, injection: {status}")
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
try:
WebDriverWait(driver, 20).until(EC.url_changes(page_url))
except TimeoutException:
# An AJAX form never changes the URL. Wait for the real success marker.
WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success, .thank-you"))
)
return answer
def main():
solver = TwoCaptcha(os.environ["TWOCAPTCHA_API_KEY"], polling_interval=5)
driver = webdriver.Chrome()
try:
answer = solve_and_submit(driver, solver)
if "reject" in driver.page_source.lower():
solver.report(answer["captchaId"], False) # refunds a bad token
finally:
driver.quit()
if __name__ == "__main__":
main()Four differences from the version this article used to print, and each one comes from a real failure. The key comes from the environment rather than a literal. page_url is captured after redirects, because solving against the pre-redirect URL is one of the most common causes of a rejected token. The token gets a budget, so a slow solve fails loudly instead of submitting something already dead. And EC.url_changes has a fallback, because it only reports success for forms that navigate; an AJAX submit leaves the URL untouched and the old script timed out on every one of them.
solver.report(captcha_id, False) is worth the two lines. A rejected token reported back is refunded, and the report feeds your account's future accuracy.
v3, and the score you cannot buy
For v3 there is usually no textarea to fill. The page calls grecaptcha.execute(sitekey, {action}), gets a token in a promise, and ships it with its own request. You pass the extra parameters to the solver and then feed the token into whatever the page does with it:
answer = solver.recaptcha(
sitekey=sitekey,
url=page_url,
version="v3",
action="submit", # the action name the page uses
score=0.4,
)The old version of this article asked for score=0.7 and moved on. That number is not achievable, and the vendor says so in its own library. The docstring for the score parameter in 2captcha-python reads, verbatim: "The score needed for resolution. Currently, it's almost impossible to get token with score higher than 0.3. Default: 0.4."
The pricing page corroborates it. 2Captcha charges $1.45 per 1,000 for v3 at score 0.3 or below and $2.99 above it, which is what a price list looks like when the expensive case is rare. CapSolver approaches the same fact from the other side: its v3 listing tells customers who cannot reach a score of 0.7 or 0.9 to raise it with support, which is not how a parameter you can set behaves. Google recommends a default threshold of 0.5. Put those three numbers side by side and the picture is plain: a purchased v3 token typically lands below the threshold Google suggests, and no parameter changes that.
That is the real reason v3 is different in kind. On v2 you are buying an answer to a puzzle. On v3 you are buying a reputation you do not have, and reputation does not come in units of $2.99.
Where this breaks
- The token expired in your own code. Two minutes from the worker's solve, minus polling lag, minus everything you do before clicking submit. Anything that navigates, waits for a lazy image or retries a flaky selector between token and submit is spending that budget.
- You solved a different URL than you submitted. Redirects, a trailing slash, a locale prefix,
httpversushttps.siteverifyreports the hostname it saw and the site can compare it. - A second widget on the page. Measured above. The fix is to index the fields rather than reach for the id.
- The callback is a dotted path or does not exist. Also measured above. On invisible reCAPTCHA the callback is the only delivery path, so getting this wrong means the field is full and nothing happens.
innerHTMLon a dirtied field. The single most expensive line in the old article.- The retry loop replays a dead token.
timeout-or-duplicateon the second attempt means solve again, not submit again. - The solver returned nothing and you never noticed.
NetworkExceptionconflates outages with queueing, and the default ten-minute reCAPTCHA timeout hides both. - The site moved to Enterprise. Same site key shape, different scoring, higher cost, and
grecaptcha.enterprisewheregrecaptchaused to be.
Test each of these once by hand before trusting a schedule. Most of them fail without an exception, which is what makes them expensive.
What changes at ten thousand pages
A script that solves one captcha and a job that solves ten thousand are different programs.
Concurrency is a queue, not a knob. Every provider caps how many jobs one account can have open, and pushing past it returns a slot error rather than queueing politely. Retrying that error immediately turns one slow minute into a retry storm. Back off, cap in-flight solves, and treat the cap as a property of the account you are on.
Budget by success rate, not by price. A run that pays $0.60 per 1,000 with an eighteen percent rejection rate is more expensive than one that pays $2.99 with two percent, because the rejected pages cost you a second solve, a second page load and a slot. Log solve time, injection status and final outcome per page. Those three columns tell you when the target changed its widget, usually a day before your success rate falls off a cliff.
Most pages should never reach the solver. If ten thousand pages trigger ten thousand challenges, the problem is not the captcha. It is that the crawler looks like a crawler on page one, and the next section is worth more than this one.
Making the challenge not appear
A challenge you never see costs nothing and takes no time. This is where the effort belongs, and it is also where the folklore is thickest.
navigator.webdriver is a standard, not a leak. The W3C WebDriver specification defines it deliberately: the webdriver-active flag "is set to true when the user agent is under remote control", and the property exists to give sites "a standard way for co-operating user agents to inform the document that it is controlled by WebDriver". Selenium is telling the truth about itself on purpose.
The popular flag works, and fixes the smaller problem. We probed a headless Chromium 141 twice through Selenium 4.47.0. Plain, navigator.webdriver was true. With --disable-blink-features=AutomationControlled, it was false. In both runs the user agent string still read HeadlessChrome/141.0.0.0. Flipping one boolean while announcing headless mode in every request header is not a disguise. Set the user agent too, or run headful under a virtual display.
Old headless is gone, so the old advice is too. Running Chrome 141 with --headless=old here returns, verbatim: "Old Headless mode has been removed from the Chrome binary." Chrome's own headless documentation dates it: "Since Chrome 132.0.6793.0 the old Headless mode is only available as a standalone binary named chrome-headless-shell." Tutorials that tell you to prefer old headless for stealth are describing a binary you no longer have, and chrome-headless-shell is the more detectable of the two.
undetected-chromedriver has not moved since February 2024. This is the tool every list still recommends first. Its latest release on PyPI is 3.5.5, uploaded 17 February 2024, two and a half years ago, against a Chrome that has gained roughly thirty major versions since. The repository is not archived and carries around 1,100 open issues. Its own README sets the expectation better than any review does: "THIS PACKAGE DOES NOT, and i repeat DOES NOT hide your IP address, so when running from a datacenter (even smaller ones), chances are large you will not pass." It still works on some targets. It is not maintained, and a package that patches a browser it has never seen is a poor bet.
The maintained successor is SeleniumBase. UC Mode is "based on undetected-chromedriver" with "multiple updates, fixes, and improvements", and the project points past it: "for the successor to plain UC Mode, see CDP Mode". SeleniumBase shipped 4.51.12 on 10 August 2026, the same week as Selenium 4.47.0. That release cadence is the whole argument.
IP reputation outranks all of it. Datacenter ranges draw challenges on the first request no matter how clean the browser is. Rotating residential proxies are the single change that moves the score most, and they usually cost more than the solver. A headless browser on a good address beats a heavily patched one on a bad address every time.
Warm sessions score better than cold ones. Reuse cookies, keep a profile, let a session accumulate history rather than arriving fresh for every page. Google's v3 documentation says the model "learns by seeing real traffic on your site", and a client with no history is exactly what it has learned to distrust. For the wider picture on what the defending side is watching, see the breakdown of anti-scraping protection.
When not to solve at all
A solver is a recurring cost, a dependency and a moving target. Rule out the alternatives first.
If the site publishes an API or a data feed, use it; it will be faster than a browser and it will not break next quarter. If you own the target, whitelist your test addresses or lower the score threshold for known traffic instead of paying to defeat your own defense. For flows behind v3, a well-behaved session on a residential address sometimes clears the bar without a single solving call, which is the cheapest outcome available.
The audio-challenge bypass that older guides still list has the same problem in a different shape. Downloading the audio file and running it through speech-to-text does still exist in a few libraries, and it is the most heavily rate-limited path Google offers, so it degrades the moment you use it at any volume. What starts as a way to avoid paying a solver service usually ends at one anyway, several days later.
And if the point of the exercise is the data rather than the pipeline, the token flow is a strange thing to own. Sites that fight back are where managed extraction earns its keep. Where the output matters more than the machinery, data as a service moves the maintenance to whoever wants it.
Four numbers decide whether any of this works: how long the token has left, whether it landed in the right field, whether the callback fired, and how many pages needed a solve at all. Instrument those four and the rest is maintenance. Ignore them and you get a script that works on your laptop and fails at three in the morning with no stack trace.