Captcha solver error handling - retries, backoff and what never to retry
A captcha solve is a network call to a system you do not control, against a challenge that can change without warning. Handling failures well is the difference between a pipeline that degrades gracefully and one that wastes time in a retry storm.
With Scarnox the economics help you: failed solves are free, so a retry costs time and nothing else. But retrying the wrong failure still wastes that time, and retrying hard enough can cause problems of its own. This guide gives you a retry policy you can paste into a Python job.
Scarnox solves PopularCaptcha today. The same error handling applies to any captcha solver, so the policy below does not depend on the captcha type.
Always check errorId first
A solve response has an errorId field. 0 means success and the token sits under solution.token. Any other value means the solve failed. A failed response also carries an errorCode and an errorDescription. Log both: the code is what you will search for later, and the description is what you will read when a run goes wrong.
{"errorId": 1, "errorCode": "...", "errorDescription": "..."}
Never assume a response is a success because the HTTP status is 200. Check errorId every time.
Failures to retry
Some failures are temporary. Retry these with exponential backoff and a small random delay:
- Network errors: connection resets, DNS hiccups, read timeouts on your side.
- Server errors: HTTP 5xx responses, which mean a problem on the service side.
- Rate limiting: HTTP 429, which means you are sending requests too fast. Slow down.
- Timeouts: the solve ran longer than your client waited. Retry with a longer timeout if you have not already set one.
These failures are worth a retry because the next attempt may succeed without any change on your side.
Failures to fix before retrying
Other failures will not go away by waiting. A retry with the same input will fail the same way. Fix these first:
- Invalid task type. Check that
captcha_typeis spelled exactly as in the docs. - Wrong or missing sitekey. Read the sitekey again from the page. A sitekey from an old deploy is a common cause.
- Wrong page URL.
site_urlshould be the page that shows the widget. - Proxy problems. A dead, mistyped or blocked proxy will fail every attempt. Test the proxy on its own first. See proxy versus proxyless.
- Account problems. An invalid key or an account without the needed balance will fail every attempt until someone fixes the account. Alert a person; do not retry in a loop.
When a failure is not on either list, treat it as a fix-first failure. Look at the error text, decide, and add the code to the right list in your code. Keep the lists short and reviewed.
A policy you can paste
import os
import random
import time
import requests
API_URL = "https://api.scarnox.com/api/tasks/create"
HEADERS = {"Authorization": f"Bearer {os.environ['SCARNOX_KEY']}"}
MAX_ATTEMPTS = 4
class SolveError(Exception):
def __init__(self, code: str, description: str, retryable: bool):
super().__init__(f"{code}: {description}")
self.code = code
self.retryable = retryable
def solve_once(payload: dict) -> str:
try:
r = requests.post(API_URL, headers=HEADERS, json=payload, timeout=185)
except requests.RequestException as exc:
# Network error or timeout on our side: safe to retry.
raise SolveError("NETWORK", str(exc), retryable=True)
if r.status_code == 429 or r.status_code >= 500:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=True)
try:
data = r.json()
except ValueError:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=False)
if data.get("errorId") == 0:
return data["solution"]["token"]
# The API reported a failure. Retry only codes you have seen to be transient.
code = data.get("errorCode", "UNKNOWN")
retryable = code in TRANSIENT_CODES
raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)
# Add API error codes here only after you have seen them to be transient in your logs.
TRANSIENT_CODES: set[str] = set()
def solve(payload: dict) -> str:
for attempt in range(MAX_ATTEMPTS):
try:
return solve_once(payload)
except SolveError as err:
last_attempt = attempt == MAX_ATTEMPTS - 1
if not err.retryable or last_attempt:
raise
delay = min(30, 2 ** attempt) + random.random() # backoff + jitter
print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
time.sleep(delay)
raise RuntimeError("unreachable")
The policy never retries a fix-first failure, and it stops after a fixed number of attempts. The TRANSIENT_CODES set starts empty on purpose: add a code only after your logs show it is temporary.
Why jitter matters
If fifty workers all fail at the same moment and all sleep exactly two seconds, they all retry together and fail together again. A random fraction of a second spreads them out. This is the same advice most large API providers give for their own clients.
Timeouts
Set your HTTP timeout well above the typical solve time, about 185 seconds. The request blocks until the token is ready, so a short client timeout can turn a slow success into a failure that you then retry for nothing. A client timeout that is longer than the solve is the safe setting.
Tokens are not cacheable
A token expires within about two minutes. Do not cache tokens between runs or reuse one for a second submission. If a submit fails after the token was used, solve again rather than resubmitting the old token. The token expiry guide covers the timing in more detail.
Concurrency and backoff across workers
If many workers share one key, the backoff in one worker does not slow the others. Add a shared limit on concurrent solves, such as a semaphore or a queue, so a burst of retries does not turn into a burst of requests. A simple concurrency cap solves most rate-limit failures before they start.
Logging that helps
For every failed attempt, log the errorCode, the attempt number, the sitekey, the page host and whether the failure was retried. Do not log the API key or a proxy password. A dashboard that counts codes per hour tells you quickly whether a problem is in your code, the proxy or the service.
Next steps
- How to solve a PopularCaptcha in Python
- Proxy or proxyless: which task type do you need?
- Keeping CI green when your app has a captcha
- Free captcha solver trial
Test your handling against a free trial: create an account and start with free trial credit, no card needed.