Tutoriales de API

Degradación elegante cuando falla la resolución de CAPTCHA

La diferencia entre un lote que pierde una URL y un lote que se cae entero está en cómo respondes al primer fallo. La resolución de un CAPTCHA no siempre sale bien: un timeout, un sitekey mal extraído, el saldo agotado o un pico de solicitudes bastan para lanzar una excepción. Si tu automatización se detiene ahí, pierdes todo el progreso acumulado. La degradación elegante elimina ese punto único de fallo: en lugar de caerse, el proceso omite la unidad problemática, la reintenta con criterio, la encola para más tarde o pasa a un modo reducido hasta que el servicio se recupera.

Piensa en un lote nocturno que monitorea precios en un marketplace regional tipo MercadoLibre. Si la URL número 300 devuelve ERROR_ZERO_BALANCE, no quieres perder las 299 anteriores ni las 700 siguientes. Los cuatro patrones de este tutorial —omitir, encolar, modo degradado y una combinación en Node.js— te dan ese margen.


Modos de fallo y cómo responder a cada uno

No todos los fallos merecen la misma reacción: unos se resuelven solos con el tiempo y otros no cambiarán por mucho que insistas. Esta tabla asocia cada código de error con su estrategia:

Fallo Código de error Estrategia de recuperación
Tiempo de espera agotado CAPCHA_NOT_READY (se agotaron los sondeos) Reintenta con un desafío nuevo
Parámetros incorrectos ERROR_BAD_PARAMETERS Registra y omite; corrige la extracción
sitekey incorrecto ERROR_WRONG_GOOGLEKEY Vuelve a extraer el sitekey
Saldo agotado ERROR_ZERO_BALANCE Pausa, alerta y espera la recarga
Límite de solicitudes ERROR_TOO_MUCH_REQUESTS Aplica backoff exponencial
API no disponible Error de conexión Disyuntor (circuit breaker) + reintento

Cuántos reintentos aplicar a cada fallo

La regla práctica: reintenta solo lo transitorio, y hazlo con un tope. Reintentar un error permanente desperdicia tiempo y satura la cola.

Tipo de fallo Reintentos recomendados Acción tras el último intento
CAPCHA_NOT_READY prolongado 1 tarea nueva tras el timeout Marcar como soft_fail y seguir con la siguiente unidad
Error de red o 5xx 2 o 3 con backoff exponencial Encolar para reprocesar cuando el upstream se estabilice
Parámetros inválidos 0 Registrar evidencia y corregir la extracción, sin insistir
Saldo o cuota 0 Pausar el lote y avisar al operador

Patrón 1: omitir y continuar

Para lotes donde perder alguna unidad es aceptable, el patrón más simple es intentar la resolución un par de veces y, si no sale, devolver None y pasar a la siguiente URL en lugar de lanzar una excepción. El lote termina; las unidades que fallaron quedan registradas para revisarlas aparte.

import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_or_skip(captcha_type, sitekey, page_url, max_retries=2):
    """Try to solve; return None on failure instead of crashing."""
    for attempt in range(max_retries):
        try:
            token = solve_captcha(captcha_type, sitekey, page_url)
            if token:
                return token
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")

    return None  # Skip this item


def process_urls(urls):
    results = []
    skipped = []

    for url in urls:
        sitekey = extract_sitekey(url)
        if not sitekey:
            skipped.append({"url": url, "reason": "no_sitekey"})
            continue

        token = solve_or_skip("recaptcha_v2", sitekey, url)
        if token:
            data = submit_form(url, token)
            results.append({"url": url, "data": data})
        else:
            skipped.append({"url": url, "reason": "solve_failed"})

    print(f"Processed: {len(results)}, Skipped: {len(skipped)}")
    return results, skipped

Patrón 2: encolar los fallos para reintentar

Cuando un fallo puede resolverse más tarde —el upstream se estabiliza, se recarga el saldo—, no lo descartes: envíalo a una cola de reintentos con backoff. Cada tarea guarda su número de intento y la hora a partir de la cual vuelve a estar lista, de modo que no la reprocesas antes de tiempo. En el lote de monitoreo de precios del ejemplo, esto significa que un pico de ERROR_TOO_MUCH_REQUESTS a medianoche no pierde datos: las URLs afectadas se reprocesan solas cuando baja la presión.

from collections import deque
import json

class RetryQueue:
    def __init__(self, max_retries=3, backoff_base=60):
        self.queue = deque()
        self.max_retries = max_retries
        self.backoff_base = backoff_base

    def add(self, task):
        task["retry_count"] = task.get("retry_count", 0) + 1
        if task["retry_count"] <= self.max_retries:
            task["retry_after"] = time.time() + (
                self.backoff_base * task["retry_count"]
            )
            self.queue.append(task)
            return True
        return False  # Exceeded max retries

    def get_ready(self):
        """Get tasks ready for retry."""
        ready = []
        remaining = deque()
        now = time.time()

        while self.queue:
            task = self.queue.popleft()
            if task["retry_after"] <= now:
                ready.append(task)
            else:
                remaining.append(task)

        self.queue = remaining
        return ready

    def save(self, filepath="retry_queue.json"):
        with open(filepath, "w") as f:
            json.dump(list(self.queue), f)

    def load(self, filepath="retry_queue.json"):
        try:
            with open(filepath) as f:
                self.queue = deque(json.load(f))
        except FileNotFoundError:
            pass


# Usage
retry_q = RetryQueue()

def process_with_retry(task):
    try:
        token = solve_captcha(task["type"], task["sitekey"], task["url"])
        if token:
            return submit_form(task["url"], token)
        else:
            retry_q.add(task)
    except Exception:
        retry_q.add(task)

# Process retry queue periodically
def drain_retry_queue():
    ready = retry_q.get_ready()
    for task in ready:
        process_with_retry(task)

Los métodos save() y load() persisten la cola en un archivo JSON; para volúmenes altos, cámbialos por Redis o una base de datos.


Patrón 3: modo degradado cuando el servicio no responde

Si el servicio de resolución deja de responder por completo, seguir lanzándole solicitudes solo acumula fallos y latencia. El modo degradado corta por lo sano: tras varios fallos seguidos, la clase deja de llamar a la API durante unos minutos y aplica una acción reducida —omitir las páginas con CAPTCHA, encolarlas o probar un solver de respaldo—. Pasado el tiempo de recuperación, vuelve a intentar con normalidad.

class CaptchaSolver:
    def __init__(self, api_key):
        self.api_key = api_key
        self.degraded = False
        self.failure_count = 0
        self.failure_threshold = 5
        self.recovery_time = None

    def solve(self, captcha_type, sitekey, page_url):
        if self.degraded:
            if time.time() < self.recovery_time:
                return self._degraded_action(page_url)
            else:
                self.degraded = False
                self.failure_count = 0

        try:
            token = self._solve_api(captcha_type, sitekey, page_url)
            self.failure_count = 0
            return token
        except Exception as e:
            self.failure_count += 1
            if self.failure_count >= self.failure_threshold:
                self._enter_degraded_mode()
            raise

    def _enter_degraded_mode(self):
        self.degraded = True
        self.recovery_time = time.time() + 300  # 5 min
        print("Entering degraded mode for 5 minutes")
        # Send alert

    def _degraded_action(self, url):
        """What to do when solving is unavailable."""
        # Option A: Skip CAPTCHA pages entirely
        return None

        # Option B: Queue for later
        # retry_queue.add({"url": url, ...})
        # return None

        # Option C: Try alternative solver
        # return self._solve_with_backup_api(...)

    def _solve_api(self, captcha_type, sitekey, page_url):
        # Normal CaptchaAI API call
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url,
            "json": "1",
        }).json()

        if resp["status"] != 1:
            raise Exception(resp["request"])

        task_id = resp["request"]
        for _ in range(24):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": "1"
            }).json()
            if result["status"] == 1:
                return result["request"]
            if result["request"] != "CAPCHA_NOT_READY":
                raise Exception(result["request"])

        raise Exception("TIMEOUT")

Node.js: los tres patrones en una sola clase

En producción rara vez usas un patrón aislado. Esta clase de Node.js combina los tres: cuenta los fallos, entra en modo degradado (con una ventana más larga para ERROR_ZERO_BALANCE), encola lo que no pudo resolver y drena la cola cuando el servicio se recupera.

class ResilientSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.retryQueue = [];
    this.failureCount = 0;
    this.degraded = false;
  }

  async solve(type, sitekey, pageUrl) {
    if (this.degraded) {
      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }

    try {
      const token = await this._callApi(type, sitekey, pageUrl);
      this.failureCount = 0;
      return token;
    } catch (err) {
      this.failureCount++;

      if (err.message === 'ERROR_ZERO_BALANCE') {
        this._enterDegraded(600000); // 10 min
        return null;
      }

      if (this.failureCount >= 5) {
        this._enterDegraded(300000); // 5 min
      }

      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }
  }

  _enterDegraded(durationMs) {
    this.degraded = true;
    console.warn(`Degraded mode for ${durationMs / 1000}s`);
    setTimeout(() => {
      this.degraded = false;
      this.failureCount = 0;
      this.drainRetryQueue();
    }, durationMs);
  }

  async drainRetryQueue() {
    const tasks = this.retryQueue.splice(0);
    for (const task of tasks) {
      await this.solve(task.type, task.sitekey, task.pageUrl);
    }
  }

  async _callApi(type, sitekey, pageUrl) {
    // Standard submit + poll
    const axios = require('axios');
    const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
      params: { key: this.apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl: pageUrl, json: 1 },
    });
    if (submit.data.status !== 1) throw new Error(submit.data.request);

    const taskId = submit.data.request;
    for (let i = 0; i < 24; i++) {
      await new Promise(r => setTimeout(r, 5000));
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: this.apiKey, action: 'get', id: taskId, json: 1 },
      });
      if (poll.data.status === 1) return poll.data.request;
      if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
    }
    throw new Error('TIMEOUT');
  }
}

Errores comunes y cómo corregirlos

Síntoma Causa Solución
Se omiten todas las tareas El modo degradado se activó demasiado pronto Sube el umbral de fallos (failure_threshold)
La cola de reintentos crece sin fin Las tareas nunca llegan a resolverse Fija un máximo de reintentos y mueve el resto a una dead letter queue
La recuperación es demasiado lenta La ventana de modo degradado es muy larga Reduce el tiempo de recuperación y añade una sonda de health check
Se pierden las tareas encoladas al reiniciar La cola vive solo en memoria Persístela en un archivo o base de datos

Preguntas frecuentes

¿Cómo distingo un fallo transitorio de uno permanente?

Los transitorios se resuelven solos con el tiempo: un CAPCHA_NOT_READY que agotó los sondeos, un ERROR_TOO_MUCH_REQUESTS o un error de red 5xx. Reintenta esos con backoff. En cambio, ERROR_BAD_PARAMETERS y ERROR_WRONG_GOOGLEKEY son permanentes: reintentar no cambia nada, porque el problema está en cómo extrajiste el sitekey o la página. Regístralos, corrige la extracción y sigue.

¿Cuántos reintentos y qué backoff conviene configurar?

Depende del tipo de fallo. Para errores de red o 5xx, 2 o 3 intentos con backoff exponencial (por ejemplo, 60 s, 120 s, 240 s) suelen bastar. Para un timeout de resolución, un único reintento con un desafío nuevo es lo razonable. Más allá de eso, encola la tarea en lugar de martillar la API: reintentar sin límite solo multiplica el gasto y satura la cola.

¿Cómo evito perder la cola de reintentos si el proceso se reinicia?

Persiste la cola fuera de memoria. En el RetryQueue del Patrón 2, los métodos save() y load() la vuelcan a un archivo JSON; para volúmenes altos, usa Redis o una base de datos. Una cola solo en memoria pierde todas las tareas pendientes en cada reinicio o despliegue.

¿La degradación elegante afecta mi facturación en CaptchaAI?

No de forma directa. CaptchaAI factura por thread concurrente, no por resolución, y cada plan incluye resoluciones ilimitadas por thread. Un reintento ocupa un thread mientras está en curso, pero no hay coste por intento ni recargo por tipo de CAPTCHA. Encolar y aplicar backoff, de hecho, ayuda a mantener el uso dentro de los threads que ya pagas —los 5 del plan BASIC ($15/mes), por ejemplo— en vez de dispararlo con reintentos agresivos.


Crea una automatización de CAPTCHA resistente con CaptchaAI

Crea tu cuenta en captchaai.com, consigue tu API key y empieza con el plan BASIC ($15/mes, 5 threads); sube de threads cuando tu volumen crezca.


Guías relacionadas

Los comentarios están deshabilitados para este artículo.