Production CAPTCHA solving fails. Networks drop, APIs time out, tokens expire. This guide covers every error pattern you'll hit with CaptchaAI and the retry strategies that keep your pipeline running.
CaptchaAI error classification
Retriable errors (retry immediately or with backoff)
| Error code | Meaning | Retry strategy |
|---|---|---|
CAPCHA_NOT_READY |
Still solving | Poll every 5s (default) |
ERROR_NO_SLOT_AVAILABLE |
Server busy | Wait 3-5s, retry |
| Network timeout | Connection lost | Retry with backoff |
| HTTP 500/502/503 | Server error | Retry with backoff |
Non-retriable errors (fix the input)
| Error code | Meaning | Action |
|---|---|---|
ERROR_WRONG_USER_KEY |
Invalid API key | Check/update key |
ERROR_KEY_DOES_NOT_EXIST |
API key not found | Verify key |
ERROR_ZERO_BALANCE |
No funds | Top up account |
ERROR_CAPTCHA_UNSOLVABLE |
Can't solve this CAPTCHA | Skip or try different params |
ERROR_BAD_DUPLICATES |
Too many duplicates | Change parameters |
ERROR_WRONG_CAPTCHA_ID |
Invalid task ID | Don't retry — task was never created |
ERROR_BAD_PARAMETERS |
Missing/wrong params | Fix request data |
Basic retry with exponential backoff
import time
import requests
API_KEY = "YOUR_API_KEY"
# Define error categories
RETRIABLE_ERRORS = {
"ERROR_NO_SLOT_AVAILABLE",
"CAPCHA_NOT_READY",
}
FATAL_ERRORS = {
"ERROR_WRONG_USER_KEY",
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE",
"ERROR_CAPTCHA_UNSOLVABLE",
"ERROR_BAD_DUPLICATES",
"ERROR_BAD_PARAMETERS",
"ERROR_WRONG_CAPTCHA_ID",
}
class CaptchaError(Exception):
"""Base CAPTCHA error."""
def __init__(self, code, message=""):
self.code = code
super().__init__(f"{code}: {message}")
class RetriableError(CaptchaError):
"""Error that can be retried."""
pass
class FatalError(CaptchaError):
"""Error that should not be retried."""
pass
def classify_error(error_code):
"""Classify an error as retriable or fatal."""
if error_code in RETRIABLE_ERRORS:
raise RetriableError(error_code)
elif error_code in FATAL_ERRORS:
raise FatalError(error_code)
else:
# Unknown errors — treat as retriable
raise RetriableError(error_code, "Unknown error")
Retry decorator
import functools
import random
def retry_captcha(max_retries=3, base_delay=2, max_delay=30, jitter=True):
"""Decorator for retrying CAPTCHA operations with exponential backoff."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except FatalError:
raise # Don't retry fatal errors
except RetriableError as e:
last_exception = e
if attempt < max_retries:
delay = min(base_delay * (2 ** attempt), max_delay)
if jitter:
delay *= (0.5 + random.random())
print(f"Retry {attempt + 1}/{max_retries} after {delay:.1f}s: {e}")
time.sleep(delay)
except requests.exceptions.RequestException as e:
last_exception = e
if attempt < max_retries:
delay = min(base_delay * (2 ** attempt), max_delay)
print(f"Network retry {attempt + 1}/{max_retries}: {e}")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
Production solver with full error handling
class RobustCaptchaSolver:
"""Production CAPTCHA solver with retry, backoff, and error classification."""
def __init__(self, api_key, max_retries=3, poll_interval=5, max_poll_time=150):
self.api_key = api_key
self.max_retries = max_retries
self.poll_interval = poll_interval
self.max_poll_time = max_poll_time
@retry_captcha(max_retries=3, base_delay=2)
def solve(self, method, **params):
"""Solve a CAPTCHA with full error handling and retry."""
task_id = self._submit(method, **params)
return self._poll(task_id)
def _submit(self, method, **params):
"""Submit task with retry on network and slot errors."""
for attempt in range(self.max_retries + 1):
try:
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}, timeout=30)
resp.raise_for_status()
data = resp.json()
if data.get("status") == 1:
return data["request"]
error_code = data.get("request", "UNKNOWN")
if error_code == "ERROR_NO_SLOT_AVAILABLE":
if attempt < self.max_retries:
delay = 3 * (attempt + 1)
print(f"No slot available, waiting {delay}s...")
time.sleep(delay)
continue
raise RetriableError(error_code)
if error_code in FATAL_ERRORS:
raise FatalError(error_code)
raise RetriableError(error_code)
except requests.exceptions.RequestException as e:
if attempt < self.max_retries:
time.sleep(2 ** attempt)
continue
raise
raise RetriableError("MAX_SUBMIT_RETRIES")
def _poll(self, task_id):
"""Poll for result with timeout."""
start = time.time()
attempts = 0
while time.time() - start < self.max_poll_time:
time.sleep(self.poll_interval)
attempts += 1
try:
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}, timeout=30)
resp.raise_for_status()
data = resp.json()
if data.get("status") == 1:
print(f"Solved after {attempts} polls ({time.time() - start:.1f}s)")
return data["request"]
error_code = data.get("request", "")
if error_code == "CAPCHA_NOT_READY":
continue
if error_code in FATAL_ERRORS:
raise FatalError(error_code)
# Unknown poll error — keep polling
continue
except requests.exceptions.RequestException:
# Network error during poll — keep trying
continue
raise TimeoutError(f"Solve timed out after {self.max_poll_time}s ({attempts} polls)")
# Usage
solver = RobustCaptchaSolver(API_KEY)
try:
token = solver.solve("userrecaptcha", googlekey="SITEKEY", pageurl="https://example.com")
print(f"Token: {token[:50]}...")
except FatalError as e:
print(f"Fatal: {e}")
except RetriableError as e:
print(f"All retries exhausted: {e}")
except TimeoutError as e:
print(f"Timeout: {e}")
Circuit breaker pattern
Prevent cascading failures when the API is down:
import time
class CircuitBreaker:
"""Stops calling the API when too many errors occur."""
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failures = 0
self.last_failure_time = 0
self.state = "closed" # closed=normal, open=blocked, half-open=testing
def can_execute(self):
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half-open"
return True
return False
# half-open — allow one test request
return True
def record_success(self):
self.failures = 0
self.state = "closed"
def record_failure(self):
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = "open"
print(f"Circuit OPEN — pausing for {self.recovery_timeout}s")
class CircuitBreakerSolver:
"""Solver with circuit breaker protection."""
def __init__(self, api_key):
self.api_key = api_key
self.breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=60)
def solve(self, method, **params):
if not self.breaker.can_execute():
raise Exception("Circuit breaker open — API appears to be down")
try:
result = self._do_solve(method, **params)
self.breaker.record_success()
return result
except FatalError:
raise
except Exception as e:
self.breaker.record_failure()
raise
def _do_solve(self, method, **params):
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}, timeout=30).json()
if submit.get("status") != 1:
error_code = submit.get("request", "UNKNOWN")
if error_code in FATAL_ERRORS:
raise FatalError(error_code)
raise RetriableError(error_code)
task_id = submit["request"]
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") in FATAL_ERRORS:
raise FatalError(result["request"])
raise TimeoutError("Timed out")
Token expiration handling
CAPTCHA tokens expire (reCAPTCHA: ~2 minutes, Turnstile: ~5 minutes). Handle this:
import time
class TokenManager:
"""Manage CAPTCHA token lifecycle — solve, cache, detect expiry."""
def __init__(self, solver, default_ttl=110):
self.solver = solver
self.default_ttl = default_ttl
self.cache = {} # key -> (token, timestamp)
def get_token(self, cache_key, method, **params):
"""Get a valid token, solving only if needed."""
if cache_key in self.cache:
token, timestamp = self.cache[cache_key]
age = time.time() - timestamp
if age < self.default_ttl:
return token
print(f"Token expired ({age:.0f}s old), re-solving...")
token = self.solver.solve(method, **params)
self.cache[cache_key] = (token, time.time())
return token
def invalidate(self, cache_key):
"""Mark token as invalid (e.g., server rejected it)."""
self.cache.pop(cache_key, None)
def solve_with_expiry_retry(self, method, submit_fn, max_attempts=2, **params):
"""Solve and submit, re-solving if server rejects the token."""
for attempt in range(max_attempts):
token = self.solver.solve(method, **params)
success = submit_fn(token)
if success:
return token
print(f"Token rejected (attempt {attempt + 1}), re-solving...")
raise Exception("Token rejected after max attempts")
Logging and monitoring
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("captcha_solver")
class LoggingSolver:
"""Solver wrapper with structured logging."""
def __init__(self, solver):
self.solver = solver
self.stats = {"solved": 0, "failed": 0, "retries": 0}
def solve(self, method, **params):
start = time.time()
url = params.get("pageurl", "unknown")
try:
token = self.solver.solve(method, **params)
elapsed = time.time() - start
self.stats["solved"] += 1
logger.info(f"Solved {method} for {url} in {elapsed:.1f}s")
return token
except FatalError as e:
self.stats["failed"] += 1
logger.error(f"Fatal error for {url}: {e}")
raise
except Exception as e:
self.stats["failed"] += 1
logger.warning(f"Failed for {url}: {e}")
raise
def report(self):
total = self.stats["solved"] + self.stats["failed"]
rate = (self.stats["solved"] / total * 100) if total else 0
logger.info(f"Stats: {self.stats['solved']}/{total} solved ({rate:.1f}%)")
Complete production pattern
# Combine all patterns
solver = LoggingSolver(
CircuitBreakerSolver(API_KEY)
)
token_mgr = TokenManager(solver, default_ttl=110)
# Usage
try:
token = token_mgr.get_token(
"login_page",
"userrecaptcha",
googlekey="SITEKEY",
pageurl="https://example.com/login",
)
print(f"Token: {token[:50]}...")
except FatalError as e:
print(f"Cannot solve: {e}")
except Exception as e:
print(f"Temporary failure: {e}")
finally:
solver.report()
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Every solve triggers 3 retries | Non-retriable error being retried | Check error classification |
| Circuit breaker keeps opening | Persistent API issue | Increase failure_threshold or check API status |
| Tokens always expire | Solve too slow + slow form submission | Pre-solve before navigating |
ERROR_ZERO_BALANCE loop |
Balance depleted during batch | Check balance before batch start |
Random ConnectionError |
Network flakiness | Add retry with backoff on network errors |
Frequently asked questions
Should I retry ERROR_CAPTCHA_UNSOLVABLE?
No. This means CaptchaAI's workers couldn't solve the CAPTCHA. Retrying the same CAPTCHA will likely fail again. Try with different parameters or skip.
What's a good max retry count?
3 retries for submission, 30 polls for result checking. More than 3 submission retries usually means a deeper issue (wrong key, zero balance).
How do I handle balance running out mid-batch?
Check your balance before starting: GET /res.php?key=KEY&action=getbalance. If balance is low, stop submitting new tasks.
Summary
Robust CAPTCHA solving with CaptchaAI requires proper error classification, exponential backoff, circuit breakers, and token expiration handling. Use the RobustCaptchaSolver + CircuitBreaker + TokenManager stack for production reliability.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.