Tutorials

CAPTCHA Solving Fallback Chains

A single CAPTCHA-solving path is a single point of failure. Fallback chains cascade through alternative strategies when the primary approach fails — different proxies, different solver methods, or degraded modes that keep your pipeline running.


Fallback chain architecture

Primary Path          Fallback 1           Fallback 2          Degraded Mode
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ Proxy Pool A │──▶ │ Proxy Pool B │──▶ │ No Proxy     │──▶ │ Skip + Log   │
│ + reCAPTCHA  │    │ + reCAPTCHA  │    │ + reCAPTCHA  │    │              │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
      ✗ fail              ✗ fail              ✗ fail              ✓ continue

Each level attempts the solve with different parameters. If all levels fail, the task enters a degraded mode (skip, queue for later, or alert).


Fallback levels

Level 1: Proxy rotation

Same solver method, different proxy:

Attempt Proxy Expected outcome
1 Residential Pool A High success rate
2 Residential Pool B Alternate IP range
3 Datacenter proxy Lower rate, but fast

Level 2: Method change

Switch the solving approach:

Original Fallback When
reCAPTCHA v2 (with proxy) reCAPTCHA v2 (no proxy) Proxy pool exhausted
Cloudflare Challenge Cloudflare Turnstile Challenge method unavailable
Standard solve Image OCR (screenshot) Token method fails

Level 3: Degraded mode

When all solving paths fail:

  • Skip and log — continue pipeline, record the failure
  • Queue for manual review — hold the task for later
  • Alert and pause — stop the pipeline, notify operator

Python implementation

import requests
import time
from dataclasses import dataclass, field
from typing import List, Optional, Callable

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"

@dataclass
class FallbackStep:
    name: str
    method: str
    params: dict
    proxy: Optional[str] = None
    proxytype: Optional[str] = None
    max_retries: int = 1
    timeout: int = 120

@dataclass
class FallbackChain:
    steps: List[FallbackStep]
    on_degraded: Optional[Callable] = None


class FallbackSolver:
    def __init__(self, api_key: str):
        self.api_key = api_key

    def solve(self, chain: FallbackChain) -> Optional[str]:
        errors = []

        for step in chain.steps:
            print(f"Trying: {step.name}")

            for attempt in range(step.max_retries + 1):
                try:
                    token = self._attempt(step)
                    if token:
                        print(f"Solved via: {step.name}")
                        return token
                except Exception as e:
                    errors.append(f"{step.name} (attempt {attempt + 1}): {e}")
                    print(f"  Failed: {e}")

        # All steps exhausted — degraded mode
        print(f"All fallbacks failed. Errors: {errors}")
        if chain.on_degraded:
            chain.on_degraded(errors)
        return None

    def _attempt(self, step: FallbackStep) -> Optional[str]:
        data = {
            "key": self.api_key,
            "method": step.method,
            "json": 1,
            **step.params
        }

        if step.proxy:
            data["proxy"] = step.proxy
            data["proxytype"] = step.proxytype

        resp = requests.post(SUBMIT_URL, data=data, timeout=15)
        result = resp.json()

        if result.get("status") != 1:
            error = result.get("error_text", result.get("request", "UNKNOWN"))
            raise Exception(error)

        task_id = result["request"]
        return self._poll(task_id, step.timeout)

    def _poll(self, task_id: str, max_wait: int) -> Optional[str]:
        elapsed = 0
        while elapsed < max_wait:
            time.sleep(5)
            elapsed += 5

            resp = requests.get(RESULT_URL, params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            }, timeout=10)
            result = resp.json()

            if result.get("status") == 1:
                return result["request"]
            if result.get("request") == "CAPCHA_NOT_READY":
                continue

            error = result.get("error_text", result.get("request", ""))
            raise Exception(error)

        raise Exception(f"Timeout after {max_wait}s")


# Define fallback chain
solver = FallbackSolver(api_key="YOUR_API_KEY")

chain = FallbackChain(
    steps=[
        FallbackStep(
            name="Residential Proxy A",
            method="userrecaptcha",
            params={"googlekey": "6Le-SITEKEY", "pageurl": "https://example.com"},
            proxy="res-a:port:user:pass",
            proxytype="HTTP",
            max_retries=1,
            timeout=90
        ),
        FallbackStep(
            name="Residential Proxy B",
            method="userrecaptcha",
            params={"googlekey": "6Le-SITEKEY", "pageurl": "https://example.com"},
            proxy="res-b:port:user:pass",
            proxytype="HTTP",
            max_retries=1,
            timeout=90
        ),
        FallbackStep(
            name="No Proxy",
            method="userrecaptcha",
            params={"googlekey": "6Le-SITEKEY", "pageurl": "https://example.com"},
            max_retries=1,
            timeout=120
        ),
    ],
    on_degraded=lambda errors: print(f"DEGRADED: {len(errors)} failures logged")
)

token = solver.solve(chain)
if token:
    print(f"Token: {token[:40]}...")
else:
    print("All fallbacks exhausted")

Node.js implementation

const axios = require("axios");

const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

class FallbackSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  async solve(steps, onDegraded) {
    const errors = [];

    for (const step of steps) {
      console.log(`Trying: ${step.name}`);

      for (let attempt = 0; attempt <= (step.maxRetries || 1); attempt++) {
        try {
          const token = await this._attempt(step);
          if (token) {
            console.log(`Solved via: ${step.name}`);
            return token;
          }
        } catch (err) {
          errors.push(`${step.name} (attempt ${attempt + 1}): ${err.message}`);
          console.log(`  Failed: ${err.message}`);
        }
      }
    }

    console.log(`All fallbacks failed. ${errors.length} errors.`);
    if (onDegraded) onDegraded(errors);
    return null;
  }

  async _attempt(step) {
    const params = {
      key: this.apiKey,
      method: step.method,
      json: 1,
      ...step.params,
    };

    if (step.proxy) {
      params.proxy = step.proxy;
      params.proxytype = step.proxytype;
    }

    const submitResp = await axios.post(SUBMIT_URL, null, {
      params,
      timeout: 15000,
    });

    if (submitResp.data.status !== 1) {
      throw new Error(submitResp.data.error_text || submitResp.data.request);
    }

    return this._poll(submitResp.data.request, step.timeout || 120000);
  }

  async _poll(taskId, maxWait) {
    let elapsed = 0;
    const interval = 5000;

    while (elapsed < maxWait) {
      await new Promise((r) => setTimeout(r, interval));
      elapsed += interval;

      const resp = await axios.get(RESULT_URL, {
        params: { key: this.apiKey, action: "get", id: taskId, json: 1 },
        timeout: 10000,
      });

      if (resp.data.status === 1) return resp.data.request;
      if (resp.data.request === "CAPCHA_NOT_READY") continue;

      throw new Error(resp.data.error_text || resp.data.request);
    }

    throw new Error(`Timeout after ${maxWait / 1000}s`);
  }
}

// Usage
(async () => {
  const solver = new FallbackSolver("YOUR_API_KEY");

  const steps = [
    {
      name: "Residential Proxy A",
      method: "userrecaptcha",
      params: { googlekey: "6Le-SITEKEY", pageurl: "https://example.com" },
      proxy: "res-a:port:user:pass",
      proxytype: "HTTP",
      maxRetries: 1,
      timeout: 90000,
    },
    {
      name: "Residential Proxy B",
      method: "userrecaptcha",
      params: { googlekey: "6Le-SITEKEY", pageurl: "https://example.com" },
      proxy: "res-b:port:user:pass",
      proxytype: "HTTP",
      maxRetries: 1,
      timeout: 90000,
    },
    {
      name: "No Proxy",
      method: "userrecaptcha",
      params: { googlekey: "6Le-SITEKEY", pageurl: "https://example.com" },
      maxRetries: 1,
      timeout: 120000,
    },
  ];

  const token = await solver.solve(steps, (errors) => {
    console.error(`DEGRADED: ${errors.length} failures`);
  });

  if (token) {
    console.log(`Token: ${token.slice(0, 40)}...`);
  }
})();

Multi-type fallback chains

Some sites change CAPTCHA types. Build chains that try different types:

chain = FallbackChain(steps=[
    FallbackStep(
        name="Turnstile (primary)",
        method="turnstile",
        params={"sitekey": "0x4AAAA-KEY", "pageurl": "https://example.com"},
        timeout=60
    ),
    FallbackStep(
        name="reCAPTCHA v2 (fallback)",
        method="userrecaptcha",
        params={"googlekey": "6Le-KEY", "pageurl": "https://example.com"},
        timeout=90
    ),
])

Monitoring fallback usage

Track which fallback level solves most tasks. If fallback 2 or 3 fires frequently, investigate the root cause:

from collections import Counter

solve_stats = Counter()

def track_solve(step_name):
    solve_stats[step_name] += 1

# After a period
for step, count in solve_stats.most_common():
    print(f"{step}: {count} solves")

# Alert if primary drops below 80%
total = sum(solve_stats.values())
primary = solve_stats.get("Residential Proxy A", 0)
if total > 0 and primary / total < 0.8:
    print("WARNING: Primary path solving below 80%")

Troubleshooting

Problem Cause Fix
Always falls to last step Primary proxy pool down Check proxy provider status
Degraded mode triggers constantly All proxy pools exhausted Add more proxies or raise retries
Slow overall solve time Sequential fallback adds latency Use parallel first-to-finish for non-dependent steps
Token works from fallback but rejected by site Proxy IP mismatch Use same proxy for solving and browsing

FAQ

Can I run fallback steps in parallel instead of sequential?

Yes, for independent steps. Use asyncio.gather (Python) or Promise.race (Node.js) and take the first success. This trades cost for speed.

How many fallback levels should I have?

Two to three is typical. More than four adds complexity without proportional reliability gains.

Should I use different API keys for different fallback levels?

Not usually. CaptchaAI tracks usage per key, and spreading across keys complicates billing. Use one key with soft_id for tracking.


Build resilient CAPTCHA workflows with CaptchaAI

Start building fallback chains at captchaai.com.


Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.