{"id":25595,"date":"2026-09-11T16:23:36","date_gmt":"2026-09-11T16:23:36","guid":{"rendered":"https:\/\/capskip.com\/?p=25595"},"modified":"2026-09-11T16:23:36","modified_gmt":"2026-09-11T16:23:36","slug":"selenium-grid-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/selenium-grid-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Selenium Grid \u4e0a\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08RemoteWebDriver\uff09"},"content":{"rendered":"<p>A Selenium Grid captcha solve works exactly like a local one, with a single difference that catches most people out. The browser runs on the node. Your test code does not. The CapSkip call happens in your test process, so the solver has to be reachable from wherever you run pytest, not from the machine hosting the browser. Get that one fact the right way round and the rest of this is the same code you would write against a local Chrome.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>A Selenium Grid you can reach. Standalone, hub and node, or fully distributed all behave the same from the client side.<\/li>\n<li>CapSkip running on a Windows machine, reachable from the machine that runs your tests.<\/li>\n<li>The Grid address. Selenium 4 listens for RemoteWebDriver requests on port 4444 by default.<\/li>\n<li>The sitekey and page URL of the site under test.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Which machine actually talks to the solver<\/h2>\n<p>Draw the three boxes before you configure anything, because two of them look interchangeable and are not.<\/p>\n<table>\n<thead>\n<tr>\n<th>Which machine<\/th>\n<th>What runs there<\/th>\n<th>Does it talk to CapSkip?<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Your test runner<\/td>\n<td>pytest, the SDK call, RemoteWebDriver<\/td>\n<td>Yes. This is the only one that does<\/td>\n<\/tr>\n<tr>\n<td>The Grid hub<\/td>\n<td>Routing and the session queue<\/td>\n<td>No. It only routes sessions<\/td>\n<\/tr>\n<tr>\n<td>The browser node<\/td>\n<td>The actual browser<\/td>\n<td>No. It never sees the solver<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>People wire this backwards surprisingly often, usually because the Grid nodes are the part running in Docker and Docker is where they expect the networking problem to be. There is no networking problem on the node. The token arrives there over the ordinary WebDriver protocol, as the argument to a script execution command, the same way any other string does.<\/p>\n<p>So the connection mode you need follows from where your tests execute. CapSkip offers Local, which binds to 127.0.0.1 and serves that device only, and Server, which binds to your network address or public IP so another box, a container host or a CI runner can reach the same Windows machine over the API. Both live under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>. Server mode changes where the solver runs and nothing else: it is still your hardware and still unmetered.<\/p>\n<table>\n<thead>\n<tr>\n<th>Where your tests run<\/th>\n<th>Which mode, and the host value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>On your own Windows machine, driving a remote Grid<\/td>\n<td>Local mode. The host value stays 127.0.0.1, even though the browser is elsewhere<\/td>\n<\/tr>\n<tr>\n<td>On a build agent on the same network as the solver<\/td>\n<td>Server mode. The host value is the solver machine&#8217;s LAN address<\/td>\n<\/tr>\n<tr>\n<td>On a hosted CI runner outside your network<\/td>\n<td>Server mode with a static public IP, key validation on, and a firewall rule<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That first row is the one worth noticing. A developer running tests locally against a shared Grid keeps the loopback address, because the solve never leaves their desk.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: point the driver at the Grid<\/h2>\n<p>Selenium 4 takes the Grid address and an options object. The old hub path suffix is not needed any more; the base URL is enough.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install selenium capskip\nfrom selenium import webdriver\n\nGRID = &quot;http:\/\/grid.internal:4444&quot;\n\noptions = webdriver.ChromeOptions()\noptions.add_argument(&quot;--no-sandbox&quot;)\n\n# command_executor is the Grid, not the browser. Everything you\n# call on this driver is a request over the wire to the node.\ndriver = webdriver.Remote(command_executor=GRID, options=options)<\/pre>\n<\/div>\n<p>Nothing about the CAPTCHA changes here. What changes is that every driver call is now a round trip, which becomes relevant in the timeout section below.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve, then inject the token<\/h2>\n<p>The solve is a local function call in your test process. The injection is a script execution on the node. Keep the two mentally separate and the code writes itself.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nimport os\nfrom capskip import CapSkip\n\n# The host is 127.0.0.1 when the tests run on the solver machine,\n# and the solver's address when they do not. It is never the node.\nsolver = CapSkip(\n    host=os.environ.get(&quot;CAPSKIP_HOST&quot;, &quot;127.0.0.1&quot;),\n    port=8080,\n)\n\nresult = solver.recaptcha(\n    sitekey=&quot;YOUR_SITEKEY&quot;,\n    url=&quot;https:\/\/example.com\/page-with-recaptcha&quot;,\n)\n\n# This runs on the node. The token travels as a script argument.\ndriver.execute_script(\n    &quot;document.getElementById('g-recaptcha-response')&quot;\n    &quot;.value = arguments[0];&quot;,\n    result[&quot;code&quot;],\n)<\/pre>\n<\/div>\n<p>Passing the token as an argument rather than building the script string around it matters more on a Grid than locally, because the script is serialised and shipped to the node. String concatenation is where quoting bugs turn into a silently empty field.<\/p>\n<p>Every reCAPTCHA variant is the same method with an extra keyword: invisible set to 1, enterprise set to 1, or version set to v3 with an action name. Turnstile and GeeTest are their own methods with the same shape, and the parameter list is in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: the session timeout that kills a slow solve<\/h2>\n<p>Here is the failure that is specific to Grid, and it is a good one. A node kills any session that has had no activity for the length of its session timeout, which defaults to 300 seconds. While the SDK is polling for an answer, your test process is not calling the driver at all. The browser sits there doing nothing, and from the node&#8217;s point of view the session looks abandoned.<\/p>\n<p>For a reCAPTCHA v2 checkbox this never comes up, because the answer usually arrives in well under a minute. It comes up on Turnstile challenge pages and GeeTest, on a busy solver, and on any run where you happen to hit the SDK&#8217;s own ceiling. That ceiling is 300 seconds, set by recaptchaTimeout, which is exactly the node default. At the default values you cannot win that one. The node&#8217;s idle clock starts before the SDK starts polling, so the session is reaped first and the next driver call fails with an invalid session id rather than with anything about CAPTCHAs.<\/p>\n<p>Three fixes, in the order worth trying them.<\/p>\n<ul>\n<li>Raise the session timeout on the node, using its session timeout option, to comfortably more than 300 seconds. This is the honest fix and it costs nothing.<\/li>\n<li>Keep the session busy while the solve runs. The synchronous client blocks, so this means moving the solve onto a thread or using the asynchronous client, then making a cheap driver call every so often to reset the idle clock. It works, at the cost of more moving parts.<\/li>\n<li>Lower the SDK&#8217;s own ceiling so it gives up first, and let your test retry. A TimeoutException from a ceiling you chose is easier to read in a report than a dead session.<\/li>\n<\/ul>\n<p>Two other Grid limits are worth knowing while you are in there. Maximum sessions per node defaults to the number of processors on that node, which is what caps your real parallelism. And a new session request that sits in the queue longer than the session request timeout, also 300 seconds by default, is rejected before a browser ever starts.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>A complete test that opens the page, solves, injects and submits. The submit happens immediately after the injection on purpose: a reCAPTCHA token is good for about two minutes, and a Grid adds round trips between every step. More on that in the guide to <a href=\"https:\/\/capskip.com\/recaptcha-token-expiration\/\">reCAPTCHA token expiration<\/a>.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install selenium capskip\nimport os\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom capskip import CapSkip\nfrom capskip.exceptions import NetworkException, TimeoutException\n\nGRID = &quot;http:\/\/grid.internal:4444&quot;\nPAGE = &quot;https:\/\/example.com\/page-with-recaptcha&quot;\n\ndef test_login_through_recaptcha():\n    options = webdriver.ChromeOptions()\n    driver = webdriver.Remote(command_executor=GRID, options=options)\n\n    try:\n        driver.get(PAGE)\n\n        # Read the sitekey off the rendered page rather than\n        # hardcoding it. It is on the node, so this is a round trip.\n        sitekey = driver.find_element(\n            By.CSS_SELECTOR, &quot;.g-recaptcha&quot;\n        ).get_attribute(&quot;data-sitekey&quot;)\n\n        solver = CapSkip(host=os.environ.get(&quot;CAPSKIP_HOST&quot;, &quot;127.0.0.1&quot;))\n        result = solver.recaptcha(sitekey=sitekey, url=PAGE)\n\n        driver.execute_script(\n            &quot;document.getElementById('g-recaptcha-response')&quot;\n            &quot;.value = arguments[0];&quot;,\n            result[&quot;code&quot;],\n        )\n        driver.find_element(By.CSS_SELECTOR, &quot;form&quot;).submit()\n\n    except NetworkException:\n        raise AssertionError(&quot;CapSkip unreachable from the test runner&quot;)\n    except TimeoutException:\n        raise AssertionError(&quot;Solve did not finish before the ceiling&quot;)\n    finally:\n        driver.quit()<\/pre>\n<\/div>\n<p>Always quit the driver in a finally block on a Grid. A local Chrome that leaks is one stray process on your own machine. A leaked Grid session holds one of that node&#8217;s session slots until the timeout reaps it, and on a node sized to four processors that is a quarter of your capacity gone until it does.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running solves across parallel sessions<\/h2>\n<p>The point of a Grid is running many browsers at once, and each of those sessions needs its own token. The solver takes concurrent submissions, so the shape that works is to keep the solve asynchronous rather than serialising your suite behind one blocking call.<\/p>\n<p>The Python SDK ships a genuine asynchronous client for this, which is worth saying plainly because the equivalent classes in the Node.js, PHP and .NET SDKs are aliases rather than separate implementations. The batching pattern is written up in <a href=\"https:\/\/capskip.com\/solve-captchas-parallel-python\/\">the guide to solving CAPTCHAs in parallel with Python<\/a>, and it applies unchanged when the browsers happen to be remote.<\/p>\n<p>Nothing about running more of them costs more. What does have a ceiling is the Grid: maximum sessions per node, and the queue timeout in front of it. Size the test parallelism to the Grid, not to the solver.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>NetworkException, connection refused on port 8080<\/td>\n<td>The solver is in Local mode and the tests run on another machine<\/td>\n<td>Switch to Server mode and set the host to the solver&#8217;s address<\/td>\n<\/tr>\n<tr>\n<td>You configured the node to reach the solver and nothing changed<\/td>\n<td>The node never calls CapSkip. Your test process does<\/td>\n<td>Point the host value at the solver from the test runner instead<\/td>\n<\/tr>\n<tr>\n<td>Invalid session id, right after a long solve<\/td>\n<td>The node reaped an idle session while the SDK was polling<\/td>\n<td>Raise the node&#8217;s session timeout above 300 seconds<\/td>\n<\/tr>\n<tr>\n<td>A new session request times out before a browser starts<\/td>\n<td>The queue is full and the request aged out<\/td>\n<td>Add nodes, or raise the session request timeout<\/td>\n<\/tr>\n<tr>\n<td>The response field is empty after the script ran<\/td>\n<td>The token was concatenated into the script string and quoting broke<\/td>\n<td>Pass it as a script argument, as in the samples above<\/td>\n<\/tr>\n<tr>\n<td>A valid token is rejected by the site<\/td>\n<td>It expired between the solve and the submit<\/td>\n<td>Submit in the next statement, with no waits in between<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY on submit<\/td>\n<td>The sitekey read off the page was empty or from the wrong element<\/td>\n<td>Check the selector, and that the widget had rendered before you read it<\/td>\n<\/tr>\n<tr>\n<td>Sessions pile up on one node<\/td>\n<td>A test crashed before quitting the driver<\/td>\n<td>Quit in a finally block, as in the full example<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Do I need to install anything on the Grid nodes?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. The nodes run browsers and nothing else. The SDK is a dependency of your test project, the solve happens in your test process, and the only thing that reaches the node is the token, as an argument to a script execution. This is also why a Linux Docker node works fine with a solver that only runs on Windows: the two never speak to each other.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">My tests run in CI. What has to be exposed?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The solver&#8217;s port, to the machine running the tests. That is Server mode with a static public IP if the runner is hosted, plus API key validation turned on and a firewall rule narrower than the whole internet. If your CI runners are self-hosted on your own network, the LAN address is enough and nothing has to leave it. Either way the Grid is unaffected, because it is not part of this path.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Why did my session die halfway through a Turnstile solve?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because the node saw no activity for the length of its session timeout and cleaned up. The default is 300 seconds and the SDK&#8217;s own ceiling for Turnstile is also 300 seconds, and the node&#8217;s idle clock starts first, so at those defaults the node always reaps the session before the SDK gives up. Raise the node&#8217;s session timeout, and consider lowering the SDK ceiling so the failure comes back as a clean exception rather than a dead session.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Is this different from solving with a local driver?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only in where things run. The solve call and the injection are identical, which is the point. The differences are operational: the node&#8217;s idle session timeout, the round trip on every driver call, and the fact that the solver address is decided by your test runner&#8217;s location. The single-browser versions are covered in <a href=\"https:\/\/capskip.com\/seleniumbase-captcha\/\">the SeleniumBase guide<\/a> and <a href=\"https:\/\/capskip.com\/undetected-chromedriver-captcha\/\">the undetected-chromedriver guide<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>On a Grid the browser moves and your code does not, so the solver address is decided by where your tests run. Point RemoteWebDriver at port 4444, call the SDK from the test process, and pass the token into the node as a script argument rather than building a string. Raise the node&#8217;s session timeout above 300 seconds so a slow solve cannot get its session reaped, submit immediately after injecting, and always quit the driver in a finally block.<\/p>\n<ul>\n<li>The framework as a whole is covered on <a href=\"https:\/\/capskip.com\/selenium-captcha-solver\/\">the Selenium CAPTCHA solver page<\/a>.<\/li>\n<li>The Python client library is covered on <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/li>\n<li>The challenge itself is covered on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<\/ul>\n<p>Worth knowing before you scale the suite out: CapSkip is a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a> that runs on hardware you already own, so a hundred parallel sessions and one cost exactly the same.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u5728 Selenium Grid \u4e0a\uff0c\u6d4f\u89c8\u5668\u5728\u4e00\u53f0\u673a\u5668\u4e0a\uff0c\u6d4b\u8bd5\u4ee3\u7801\u5728\u53e6\u4e00\u53f0\u673a\u5668\u4e0a\uff0c\u8fd9\u5bf9\u9a8c\u8bc1\u7801\u8bc6\u522b\u53ea\u6539\u53d8\u4e86\u4e00\u4ef6\u4e8b\uff1a\u8bc6\u522b\u5de5\u5177\u9700\u8981\u80fd\u4ece\u54ea\u91cc\u88ab\u8bbf\u95ee\u5230\u3002\u4e0b\u9762\u662f RemoteWebDriver \u7684\u914d\u7f6e\u3001token \u7684\u6ce8\u5165\u65b9\u5f0f\uff0c\u4ee5\u53ca\u90a3\u4e2a\u4f1a\u6084\u6084\u6390\u6389\u6162\u8bc6\u522b\u7684\u8282\u70b9\u4f1a\u8bdd\u8d85\u65f6\u3002<\/p>","protected":false},"author":1,"featured_media":25594,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Selenium Grid CAPTCHA: Solve on Remote Nodes | CapSkip","rank_math_description":"A selenium grid captcha solve runs in your test process, not on the node. Here is where the solver has to be reachable from, and the timeout that kills it.","rank_math_focus_keyword":"selenium grid captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25595","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25595","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25595"}],"version-history":[{"count":3,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25595\/revisions"}],"predecessor-version":[{"id":25600,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25595\/revisions\/25600"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25594"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25595"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25595"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25595"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}