SurSolver

BlogProxies for CAPTCHA Solving: Sticky Sessions and IP Matching

Proxies for CAPTCHA Solving: Sticky Sessions and IP Matching

· Updated · 7 min read

Some captcha results only work from the IP address that earned them. This post shows which SurSolver task types take your proxy, how to choose and pin one, how to send it to createTask, and how to find out why a proxy fails before it costs you tasks.

When a task needs your proxy

The proxies guide sorts every type into one of three modes:

  • Required. Cloudflare Challenge, Vercel, CaptchaFox, Akamai, DataDome and PerimeterX have no proxyless name. A proxyless name for any of them answers ERROR_TYPE_NOT_SUPPORTED.
  • Optional. reCAPTCHA v2 and v3, Turnstile, FriendlyCaptcha, Prosopo, GeeTest and MTCaptcha. The …TaskProxyLess name never needs a proxy. The proxy-capable …Task name does: sending it without proxy details is rejected with ERROR_INVALID_REQUEST.
  • None. Altcha is proxyless only.

If a type offers a proxyless name, start there. The research behind the Turnstile guide says a proxy helps only when the receiving site ties the token to an IP, and the symptom is specific: a valid-looking token is refused, while the same flow in your own browser works. If the solve itself fails, a proxy is not the fix.

One exit IP for the whole flow

For the cookie types the flow has four steps: load the page and trigger the block, create the task, receive the solution, replay it. The proxy in the task is the IP the solve runs from, so every request you make yourself must leave from the same address. The Cloudflare challenge guide puts it plainly: cf_clearance is bound to the exit IP and the user agent, and a rotating proxy makes Cloudflare treat you as cookieless again.

Token types behave the same when a site checks the IP. The CaptchaFox research note records the failure as invalid-ip-match on the site's server-side verification: the token was earned from one address and submitted from another.

One sticky proxy session: the blocked GET, the createTask call and the replayed GET all leaving from the same exit IP

Sticky or rotating

A rotating gateway hands out a different exit IP per request or per interval. A sticky session keeps one IP for a period. For anything that must match, rotating turns "same IP" into a coin flip, so use a sticky session or a static address.

How you pin a session is specific to your proxy provider; look for "sticky session" in its documentation. SurSolver's docs do not state a minimum session length. Other solvers' documents in our research ask for sessions of roughly three to ten minutes, so treat five minutes as a starting point and size it to cover the block, the solve and every replayed request. Interactive challenges take longer than managed ones.

Keep one session per flow: its own IP, its own user agent, its own cookies. Do not share a pinned IP between unrelated flows on the same site, and do not change anything mid-flow.

Choose the proxy class

The sources do not rank proxy vendors and neither does this post. They do agree on classes:

  • Datacenter. The proxies guide says anti-bot vendors commonly refuse datacenter ranges. The Akamai, DataDome and PerimeterX notes say the same, and the reCAPTCHA v3 note reports lower scores for datacenter and VPN addresses. One solver's guidance reserves datacenter proxies for low-protection targets and smoke tests.
  • Residential. The research for DataDome, Akamai and PerimeterX converges on residential, mobile or static ISP addresses held in a sticky session.
  • Static ISP. A static ISP proxy keeps one address, so there is no session to pin.
  • Mobile. One solver's guidance reaches for mobile only when residential addresses are being flagged.

Match the country to the site's audience and to the Accept-Language you send. The Akamai note advises aligning the language with the proxy region, and the reCAPTCHA v3 note reports better scores from same-country addresses. Both are hedged in the sources, so test against your target.

Send the proxy to createTask

task.proxy takes a string. The scheme is http, https, socks4 or socks5, and http is assumed without one:

snippet.txt
http://user:password@203.0.113.10:8000
socks5://user:password@203.0.113.10:1080
user:password@203.0.113.10:8000
203.0.113.10:8000

A string with exactly four colon-separated parts is always read as user:password:host:port. Many providers hand out host:port:user:password instead, and that string is not rejected when it happens to parse the other way. 203.0.113.10:8000:user:12345 becomes user 203.0.113.10, password 8000, host user, port 12345. The task is accepted, charged, and run through the wrong proxy.

The split fields avoid the problem because nothing depends on order. They are read only when task.proxy is absent or empty:

snippet.json
{
  "clientKey": "YOUR_API_KEY",
  "task": {
    "type": "ReCaptchaV2Task",
    "websiteURL": "https://example.com/login",
    "websiteKey": "6Le...",
    "proxyType": "http",
    "proxyAddress": "203.0.113.10",
    "proxyPort": 8000,
    "proxyLogin": "user",
    "proxyPassword": "password"
  }
}

proxyAddress and proxyPort go together, proxyType is one of the four schemes, and proxyLogin and proxyPassword are optional. The proxy belongs to the task, not to the account: every createTask that needs one carries its own details.

This helper converts a host:port:user:password string into split fields, creates the task and polls every 3 seconds. The 180-second deadline is yours to choose; the docs state no maximum solve time.

solve.py
import os
import time

import requests

API = 'https://api.sursolver.com'
POLL_SECONDS = 3
DEADLINE_SECONDS = 180
REQUEST_TIMEOUT_SECONDS = 30


def call(path: str, body: dict) -> dict:
    response = requests.post(f'{API}{path}', json=body, timeout=REQUEST_TIMEOUT_SECONDS)
    response.raise_for_status()  # HTTP 500 carries no errorId: a transport failure
    data = response.json()
    if data.get('errorId'):
        raise RuntimeError(f"{data['errorCode']}: {data['errorDescription']}")
    return data


def split_fields(raw: str) -> dict:
    """Turn host:port:user:password into the split proxy fields."""
    host, port, login, password = raw.split(':', 3)
    return {
        'proxyType': 'http',
        'proxyAddress': host,
        'proxyPort': int(port),
        'proxyLogin': login,
        'proxyPassword': password,
    }


def solve(raw_proxy: str) -> dict:
    key = os.environ['SURSOLVER_API_KEY']
    task = {
        'type': 'ReCaptchaV2Task',
        'websiteURL': 'https://example.com/login',
        'websiteKey': '6Le...',
        **split_fields(raw_proxy),
    }
    task_id = call('/createTask', {'clientKey': key, 'task': task})['taskId']
    deadline = time.monotonic() + DEADLINE_SECONDS
    while time.monotonic() < deadline:
        time.sleep(POLL_SECONDS)
        result = call('/getTaskResult', {'clientKey': key, 'taskId': task_id})
        if result['status'] == 'ready':
            return result['solution']
    raise TimeoutError(f'task {task_id} still processing after {DEADLINE_SECONDS}s')

IP-authenticated proxies

A bare host:port is how IP-allowlisted proxies are used: the proxy trusts a list of source addresses instead of a login. The SurSolver docs do not publish the addresses that solves run from, so you have no list to give your proxy provider. Use a proxy with username and password credentials, and ask support before you depend on an allowlist.

Test the proxy before you submit

A task is charged when it is accepted, and a task that finishes as failed automatically returns its original charge once. A dead proxy prevents a useful result. Check it from your own machine first. https://ip-echo.example/ip stands for any service that returns the caller's IP address; use one you trust:

solve.sh
PROXY='http://user:password@203.0.113.10:8000'
curl -sS -x "$PROXY" https://ip-echo.example/ip
sleep 30
curl -sS -x "$PROXY" https://ip-echo.example/ip

Both answers should show the proxy's address, not yours, and they should match. If the second differs, the session is not sticky.

When a proxy fails

What you see Likely cause What to do
ERROR_INVALID_REQUEST from createTask Unparseable string, unknown scheme, missing or non-numeric port, or a proxy-capable name without a proxy Fix the named field. Nothing was created or charged.
Accepted, then ERROR_FAILED The solve failed; errorDescription carries the reason. The target may have blocked the proxy IP Submit a new task through a different proxy. It is a new charge.
ready, but the target refuses the result A different IP or user agent on replay, a rotating gateway, or a client whose TLS differs from a browser Replay from the same exit with solution.userAgent. See headers and user agents.
DataDome challenge URL has t=bv The IP is banned at network level and solving cannot clear it Rotate the IP and trigger the block again.
Task ran through the wrong proxy A four-part string in the other order Use the split fields.

When blocks are frequent rather than occasional, the cause is usually your request pattern, not one proxy. Stop getting blocked while scraping covers that.

Next steps

Pick the type you need on its solver page and check its proxy mode in the proxies guide. Then send a createTask with split fields, and read Errors for what each failure means.

More articles