How to Set Up CAPTCHA Proxy Rotation in Python and Node

captcha proxy rotation - 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.

TypeProxy supportedWhy
reCAPTCHA v2 and v3, including EnterpriseYesThe solve involves a request to Google from the proxy
Cloudflare TurnstileYesSame, against Cloudflare
GeeTest v3YesSame, against the GeeTest API server
Image or OCRNoThe 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 IP

Node 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 back

PHP 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 last

Three 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.

FieldValueDefault
proxyIP:PORT or login:pass@IP:PORTNone
proxytypeHTTP, HTTPS, SOCKS5 or SOCKS5HHTTP
# 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

SymptomCauseFix
Solves succeed, tokens rejectedThe page was fetched from a different IP than the solvePin one proxy across fetch, solve and submit
ERROR_CAPTCHA_UNSOLVABLE on one IP onlyThat exit node is blocked by the targetDrop it from the pool and retry on the next
Timeouts on every proxied jobproxytype does not match the proxySend the field explicitly, not the HTTP default
Proxy appears to do nothingThe job is an image CAPTCHAExpected. Proxies apply to the other three families
Works locally, fails in a containerDNS resolves differently inside the networkUse 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.