How to Set Up CAPTCHA Proxy Rotation in Python and Node

CAPTCHA proxy rotation is something you build in your own code. The solver takes one proxy per job and uses it for that job only, so the pool, the rotation policy and the retry logic all live on your side. Two rules do most of the work: pin one IP to one session instead of rotating on every call, and rotate only after a failure. This guide covers the exact proxy shape in each SDK, the raw API pair behind it, and the one CAPTCHA type where a proxy is silently ignored.
Proxies apply to three CAPTCHA families, not all four
Lead with the constraint, because it saves an afternoon of debugging. CapSkip solves four families of CAPTCHA, and proxies work on three of them: reCAPTCHA, Turnstile and GeeTest. Image CAPTCHAs do not take one.
reCAPTCHA counts once here. v2 checkbox, Invisible, Enterprise and v3 are options on the same solve call, not separate types, so they all behave the same way about proxies.
| Type | Proxy supported | Why |
|---|---|---|
| reCAPTCHA v2 and v3, including Enterprise | Yes | The solve involves a request to Google from the proxy |
| Cloudflare Turnstile | Yes | Same, against Cloudflare |
| GeeTest v3 | Yes | Same, against the GeeTest API server |
| Image or OCR | No | The image is already in hand, so no outbound request is made |
Passing a proxy to an image job is not an error. It is accepted and ignored, which is worse, because a rotation bug there produces no signal at all. If you route image CAPTCHAs through the same wrapper as everything else, skip the proxy argument on that path.
The proxy shape in each SDK
Every SDK takes the same two-field object: a type and a URI. The URI is either host and port, or credentials followed by host and port.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# The proxy applies to this one solve, nothing is remembered.
result = solver.recaptcha(
sitekey="YOUR_SITEKEY",
url="https://example.com/page-with-recaptcha",
proxy={"type": "HTTPS", "uri": "user:[email protected]:3128"},
)
print(result["code"]) # token, solved through that IPNode takes the same object as an options argument.
// npm install capskip
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const result = await solver.turnstile(
'YOUR_SITEKEY',
'https://example.com/page-with-turnstile',
{ proxy: { type: 'SOCKS5', uri: '1.2.3.4:1080' } },
);
console.log(result.code, result.userAgent); // send both backPHP nests it in the options array as a proxy key, and .NET passes a Proxy object built from the same two values. The four allowed type values are the same everywhere: HTTP, HTTPS, SOCKS5 and SOCKS5H. SOCKS5H resolves DNS at the proxy rather than locally, which is what you want when the target resolves differently by region.
Pin one proxy per session, not per request
The instinct is to pull a fresh IP for every solve. It is the wrong default and it is what gets sessions flagged.
A protected page ties a token to the context it was issued in. If your crawler loads the page from one IP and the token is solved from another, the site sees a mismatch between the browsing session and the verification. Some sites ignore it. Cloudflare and reCAPTCHA Enterprise do not, and the token validates as low quality or fails outright.
So the unit of rotation is the session, not the call. One proxy fetches the page, solves the CAPTCHA, and submits the form. The next session takes the next proxy.
# pip install capskip
import itertools
# One entry per exit IP. Round-robin, not random:
# random picks repeat, and a repeat is what a rate limiter sees.
POOL = [
{"type": "HTTPS", "uri": "user:[email protected]:3128"},
{"type": "HTTPS", "uri": "user:[email protected]:3128"},
{"type": "SOCKS5H", "uri": "user:[email protected]:1080"},
]
pool = itertools.cycle(POOL)
def new_session():
"""Hand the same proxy to the HTTP client and the solver."""
return next(pool)Round-robin beats random selection here for one reason: random sampling from a small pool repeats the same IP back to back often enough to matter, and back-to-back requests from one exit node are exactly the pattern rate limiters watch for.
Rotate on failure, not on a timer
The second rule is when to move on. Rotating every N requests throws away working IPs and keeps broken ones in circulation for up to N more calls. Rotate on evidence instead.
# pip install capskip
from capskip import CapSkip
from capskip import ApiException, NetworkException, TimeoutException
solver = CapSkip(host="127.0.0.1", port=8080)
def solve_with_retry(sitekey, url, attempts=3):
last = None
for _ in range(attempts):
proxy = new_session()
try:
return solver.recaptcha(sitekey=sitekey, url=url, proxy=proxy)
except (ApiException, NetworkException, TimeoutException) as err:
# Burn this IP for the run and take the next one.
last = err
raise lastThree attempts is the right ceiling for most pools. If a sitekey fails on three separate IPs, the problem is not the proxy: the sitekey is stale, the page URL is wrong, or the site changed its challenge. Retrying a fourth time just spends solve capacity confirming it.
Which exception you catch tells you what happened. A NetworkException means the solver was unreachable or the job was polled before it was ready. A TimeoutException means the polling window expired. An ApiException carries a returned error code, and that is the one worth logging with the proxy attached, because it is how you find the one dead exit node in a pool of forty.
The raw API: proxy and proxytype
Without an SDK, the same thing is two extra form fields on the submit call. The SDK object maps straight onto them.
| Field | Value | Default |
|---|---|---|
| proxy | IP:PORT or login:pass@IP:PORT | None |
| proxytype | HTTP, HTTPS, SOCKS5 or SOCKS5H | HTTP |
# No install step. curl is already on your machine. curl -X POST http://127.0.0.1:8080/in.php \ -d "key=YOUR_API_KEY" \ -d "method=userrecaptcha" \ -d "googlekey=YOUR_SITEKEY" \ -d "pageurl=https://example.com/page-with-recaptcha" \ -d "proxy=user:[email protected]:3128" \ -d "proxytype=HTTPS" OK|2122988149 # submitted, and it will solve through that IP
The proxytype default is HTTP, so an HTTPS or SOCKS5 proxy sent without it will be dialled as plain HTTP and fail. Send the field every time rather than relying on the default matching your pool.
Swap the host and the same call works against a shared instance. CapSkip listens on 127.0.0.1 in Local mode by default, and the connection settings also offer Server mode, where it listens on your network or public IP so other machines can reach the API. Put it on a VPS and one solver serves every worker in your crawl fleet. Proxy handling does not change: the proxy still applies per job, and the pool still lives in your own code.
Common errors and what they mean
| Symptom | Cause | Fix |
|---|---|---|
| Solves succeed, tokens rejected | The page was fetched from a different IP than the solve | Pin one proxy across fetch, solve and submit |
ERROR_CAPTCHA_UNSOLVABLE on one IP only | That exit node is blocked by the target | Drop it from the pool and retry on the next |
| Timeouts on every proxied job | proxytype does not match the proxy | Send the field explicitly, not the HTTP default |
| Proxy appears to do nothing | The job is an image CAPTCHA | Expected. Proxies apply to the other three families |
| Works locally, fails in a container | DNS resolves differently inside the network | Use SOCKS5H so the proxy resolves the hostname |
Full parameter details for each type are in the API documentation.
FAQ
Do I need a proxy to solve CAPTCHAs at all?
No. Proxyless solving works for most sites and is one less moving part. Add proxies when the target is geo-restricted, when it rate limits by IP, or when tokens start being rejected despite solving cleanly. Start without one and add it as a fix for an observed problem.
Residential or datacentre proxies?
Datacentre IPs are faster and cheaper, and they are fine on sites that only rate limit. Residential IPs matter when the target scores the network itself, which is common on Cloudflare-protected pages and reCAPTCHA Enterprise. Mixing both in one pool works: try datacentre first, fall back to residential on failure.
What is the difference between SOCKS5 and SOCKS5H?
SOCKS5 resolves the hostname on your machine and sends the resulting IP to the proxy. SOCKS5H sends the hostname and lets the proxy resolve it. Use SOCKS5H when the site returns different addresses by region, or when your local resolver cannot see the target at all.
Does the proxy slow the solve down?
A little, and it depends on the exit node rather than the solver. The polling itself is unaffected: the SDKs start at 250 milliseconds and back off to the pollingInterval ceiling, so a fast solve is still returned fast. A slow proxy shows up as a longer time to the first successful poll, not as extra polling overhead.
The short version
Pin one proxy per session, rotate when a solve fails rather than on a schedule, always send proxytype, and skip the proxy entirely on image jobs. Because the solving itself runs locally as an unlimited captcha solver, a retry costs nothing but the proxy bandwidth, which makes rotate-on-failure cheap enough to be the default. For the wider pipeline this sits in, see CAPTCHA solving for web scraping, which covers session handling end to end. The Python integration guide has the client-side setup.
