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
- Patrón de disyuntor para llamadas a la API de CAPTCHA
- Cola de mensajes fallidos (dead letter queue) para tareas fallidas
- Implementa la lógica de reintentos con la API de CaptchaAI