La regla de oro con los callbacks de CaptchaAI es corta: nunca dependas solo de ellos. Un pingback te ahorra el sondeo constante, pero si tu servidor está caído, devuelve un error o agota el tiempo de espera cuando CaptchaAI entrega el resultado, esa resolución puede perderse. Para evitarlo montamos tres capas de resiliencia:
- Sondeo alternativo — recupera las tareas cuyo callback nunca llegó.
- Dead letter queue — conserva los resultados que fallan al procesarse.
- Manejador idempotente — evita procesar dos veces la misma entrega.
¿Qué puede fallar en la entrega de un callback?
Piensa en un worker que resuelve reCAPTCHA v2 para el QA de tus checkouts: si un despliegue reinicia el servidor mientras CaptchaAI entrega resultados, esas resoluciones se pierden y hay que reenviarlas. Estos son los fallos habituales:
| Modo de fallo | Síntoma | Consecuencia |
|---|---|---|
| Servidor caído | CaptchaAI recibe conexión rechazada | La resolución no se entrega |
| El servidor devuelve 5xx | CaptchaAI recibe una respuesta de error | Puede no reintentar (según la implementación) |
| Tiempo de espera de red | La conexión de CaptchaAI se queda colgada | Resolución potencialmente perdida |
| El manejador se cae | La solicitud se acepta pero el resultado no se guarda | La resolución se pierde en silencio |
En todos los casos:
- El resultado existe en CaptchaAI, pero no siempre llega a tu sistema.
- Por eso ningún callback debe ser tu única vía de entrega.
Patrón 1: callback con sondeo alternativo
Acepta el callback cuando llega y sondea las tareas que no reciben respuesta dentro de un tiempo de espera: el callback es el camino rápido y el sondeo, la red de seguridad.
En Python, con Flask y un hilo de sondeo en segundo plano:
import os
import time
import threading
import requests
from flask import Flask, request
app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Track task state
pending_tasks = {} # task_id -> {"submitted_at": timestamp, "status": "pending"}
results = {}
lock = threading.Lock()
def submit_captcha(sitekey, pageurl, callback_url):
"""Submit with callback, but track for fallback polling."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": callback_url,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
task_id = data["request"]
with lock:
pending_tasks[task_id] = {
"submitted_at": time.time(),
"status": "pending"
}
return task_id
return None
@app.route("/callback")
def captcha_callback():
"""Primary result delivery — CaptchaAI sends results here."""
task_id = request.args.get("id")
solution = request.args.get("code")
with lock:
results[task_id] = solution
pending_tasks.pop(task_id, None)
return "OK", 200
def fallback_poller():
"""Poll for any tasks that missed their callback."""
while True:
time.sleep(30) # Check every 30 seconds
with lock:
stale_tasks = [
tid for tid, info in pending_tasks.items()
if time.time() - info["submitted_at"] > 120 # 2 min callback timeout
and info["status"] == "pending"
]
for task_id in stale_tasks:
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
with lock:
results[task_id] = data["request"]
pending_tasks.pop(task_id, None)
print(f"Fallback poll recovered: {task_id}")
elif data.get("request") != "CAPCHA_NOT_READY":
# Permanent error — remove from pending
with lock:
pending_tasks.pop(task_id, None)
print(f"Task failed: {task_id} — {data.get('request')}")
# Start fallback poller in background
poller_thread = threading.Thread(target=fallback_poller, daemon=True)
poller_thread.start()
La misma lógica en Node.js con Express y Axios:
const express = require("express");
const axios = require("axios");
const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const pendingTasks = new Map(); // taskId -> { submittedAt, status }
const results = new Map();
async function submitCaptcha(sitekey, pageurl, callbackUrl) {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
pingback: callbackUrl,
json: 1,
},
});
if (resp.data.status === 1) {
const taskId = resp.data.request;
pendingTasks.set(taskId, {
submittedAt: Date.now(),
status: "pending",
});
return taskId;
}
return null;
}
// Primary callback endpoint
app.get("/callback", (req, res) => {
const taskId = req.query.id;
const solution = req.query.code;
results.set(taskId, solution);
pendingTasks.delete(taskId);
res.sendStatus(200);
});
// Fallback poller
setInterval(async () => {
const now = Date.now();
const staleTasks = [];
for (const [taskId, info] of pendingTasks) {
if (now - info.submittedAt > 120000 && info.status === "pending") {
staleTasks.push(taskId);
}
}
for (const taskId of staleTasks) {
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId, json: 1 },
});
if (resp.data.status === 1) {
results.set(taskId, resp.data.request);
pendingTasks.delete(taskId);
console.log(`Fallback recovered: ${taskId}`);
} else if (resp.data.request !== "CAPCHA_NOT_READY") {
pendingTasks.delete(taskId);
console.log(`Task failed: ${taskId} — ${resp.data.request}`);
}
} catch (err) {
console.error(`Poll error for ${taskId}: ${err.message}`);
}
}
}, 30000);
app.listen(3000);
Patrón 2: dead letter queue para resultados problemáticos
Cuando tu manejador procesa un resultado pero se topa con un error (la base de datos está caída, falla una validación), no descartes los datos: muévelos a una dead letter queue y reintenta cuando el problema de fondo esté resuelto.
En Python, escribiendo cada fallo a disco:
import json
import os
import time
from pathlib import Path
DEAD_LETTER_DIR = Path("dead_letter")
DEAD_LETTER_DIR.mkdir(exist_ok=True)
@app.route("/callback")
def captcha_callback_with_dlq():
task_id = request.args.get("id")
solution = request.args.get("code")
try:
# Attempt normal processing
store_result(task_id, solution)
return "OK", 200
except Exception as e:
# Processing failed — save to dead-letter queue
dead_letter = {
"task_id": task_id,
"solution": solution,
"error": str(e),
"received_at": time.time()
}
dlq_path = DEAD_LETTER_DIR / f"{task_id}.json"
dlq_path.write_text(json.dumps(dead_letter))
print(f"DLQ: {task_id} — {e}")
return "OK", 200 # Still return 200 to CaptchaAI
def reprocess_dead_letters():
"""Retry processing dead-letter items."""
for dlq_file in DEAD_LETTER_DIR.glob("*.json"):
item = json.loads(dlq_file.read_text())
try:
store_result(item["task_id"], item["solution"])
dlq_file.unlink() # Remove after successful processing
print(f"DLQ reprocessed: {item['task_id']}")
except Exception:
pass # Leave in DLQ for next retry
Y la versión equivalente en Node.js:
const fs = require("fs");
const path = require("path");
const DLQ_DIR = path.join(__dirname, "dead_letter");
if (!fs.existsSync(DLQ_DIR)) fs.mkdirSync(DLQ_DIR);
app.get("/callback-dlq", (req, res) => {
const taskId = req.query.id;
const solution = req.query.code;
try {
storeResult(taskId, solution);
res.sendStatus(200);
} catch (err) {
// Save to dead-letter queue
const deadLetter = {
task_id: taskId,
solution: solution,
error: err.message,
received_at: Date.now(),
};
fs.writeFileSync(
path.join(DLQ_DIR, `${taskId}.json`),
JSON.stringify(deadLetter)
);
console.log(`DLQ: ${taskId} — ${err.message}`);
res.sendStatus(200); // Still acknowledge to CaptchaAI
}
});
function reprocessDeadLetters() {
const files = fs.readdirSync(DLQ_DIR).filter((f) => f.endsWith(".json"));
for (const file of files) {
const filePath = path.join(DLQ_DIR, file);
const item = JSON.parse(fs.readFileSync(filePath, "utf8"));
try {
storeResult(item.task_id, item.solution);
fs.unlinkSync(filePath);
console.log(`DLQ reprocessed: ${item.task_id}`);
} catch (err) {
// Leave in DLQ
}
}
}
// Retry DLQ every 5 minutes
setInterval(reprocessDeadLetters, 300000);
Patrón 3: manejador de callback idempotente
Un callback puede llegar más de una vez. Haz tu manejador idempotente para que procesar el mismo resultado dos veces no tenga efectos:
@app.route("/callback")
def idempotent_callback():
task_id = request.args.get("id")
solution = request.args.get("code")
with lock:
# Only process if not already handled
if task_id in results:
return "OK", 200 # Already processed — skip silently
results[task_id] = solution
pending_tasks.pop(task_id, None)
return "OK", 200
Qué patrón elegir según tu escenario
- Volumen bajo con caídas ocasionales: callback con sondeo alternativo.
- Alto volumen con posibles cortes de la base de datos: dead letter queue.
- Varios consumidores procesan el mismo resultado: manejador idempotente.
- Sistema en producción con SLA: los tres combinados.
Diagnóstico de problemas frecuentes
- El sondeo alternativo reprocesa tareas ya entregadas. Hay una carrera entre el callback y el sondeador; añade la comprobación de idempotencia sobre
results. - La DLQ crece sin procesarse. El reprocesador no corre o falla; revisa sus logs y confirma que la BD ya esté sana.
- El callback devuelve 200 pero el resultado se pierde. El manejador se cae después de responder; procesa antes de responder o usa la DLQ.
- Demasiadas solicitudes de sondeo alternativo. Hay muchas tareas obsoletas; sube el umbral de timeout del callback y revisa el uptime.
Preguntas frecuentes
¿Cómo evito procesar dos veces la misma resolución?
Guarda cada resultado por su task_id y comprueba si ya existe antes de procesarlo, como en el Patrón 3. La comprobación sobre results evita cobros o inserciones repetidas cuando un callback llega dos veces o coincide con el sondeo.
¿Qué código HTTP debe devolver mi endpoint de callback?
Siempre 200. Un 4xx o 5xx no ayuda, porque CaptchaAI puede no reintentar la entrega. Acepta con un 200 OK y gestiona los fallos por dentro con la DLQ o el sondeo alternativo.
¿Dónde conviene guardar la dead letter queue en producción?
En disco basta para volúmenes bajos, pero en producción usa un almacén compartido y duradero: una tabla en tu base de datos, una lista en Redis o una cola gestionada. Así varios workers ven la misma DLQ y sobrevive a un reinicio.
¿El sondeo alternativo consume threads o saldo adicional?
No. Un thread se ocupa mientras el CAPTCHA está en resolución y se libera al terminar; consultar el resultado con res.php no abre un thread nuevo ni cobra por solicitud. CaptchaAI factura por thread concurrente, con resoluciones ilimitadas por thread.
Próximos pasos
Monta una entrega de resultados a prueba de fallos: obtén tu clave API de CaptchaAI e implementa los tres patrones. Amplía con estas guías:
- Guía de la URL de callback y los webhooks
- Patrones de notificación de tareas con pingback
- Seguridad de webhooks: validación de callbacks
Artículos relacionados
- Patrones de reintento ante errores al resolver CAPTCHA en Python
- Validación de seguridad en callbacks y webhooks de CaptchaAI
- Referencia de códigos de error de CaptchaAI