Tutoriales

Eventos enviados por el servidor para notificaciones de resolución CAPTCHA en tiempo real

¿Quieres que la solución de un CAPTCHA llegue a tu cliente en el instante en que CaptchaAI la resuelve, sin sondear res.php una y otra vez? Los Server-Sent Events (SSE) mantienen una conexión HTTP abierta por la que tu servidor empuja el resultado en cuanto llega: cero solicitudes desperdiciadas y entrega en menos de un segundo, encajando con el modelo de pingback de CaptchaAI.

En esta guía montamos el circuito completo:

  • un servidor que recibe el callback de CaptchaAI y lo reenvía por SSE, en Flask y en Express;
  • un cliente EventSource que muestra cada resolución al llegar;
  • el escalado con Redis para varias instancias.

SSE, WebSocket o sondeo: cuál elegir

Los resultados viajan solo del servidor al cliente, donde SSE gana a las alternativas:

Característica SSE WebSocket Sondeo
Dirección Servidor → Cliente Bidireccional Cliente → Servidor
Protocolo HTTP/1.1+ WS/WSS HTTP
Reconexión automática Incorporada Manual N/A
Soporte del navegador Todos los modernos Todos los modernos Todos
Complejidad Baja Media Baja
Solicitudes desperdiciadas Ninguna Ninguna Muchas
Ideal para resultados CAPTCHA Excesivo Funciona, pero desperdicia

Cómo encaja SSE en el flujo de resolución de CAPTCHA

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. El cliente se conecta a tu endpoint SSE (una conexión HTTP persistente)
  2. El cliente envía una tarea CAPTCHA a CaptchaAI con pingback apuntando a tu servidor
  3. CaptchaAI resuelve el CAPTCHA y envía el resultado a tu endpoint de callback
  4. Tu servidor reenvía el resultado por el flujo SSE hasta el cliente

Piensa en un panel donde un equipo de operaciones vigila decenas de tareas de reCAPTCHA v2 o Cloudflare Turnstile: cada resultado aparece en cuanto CaptchaAI lo devuelve, sin sondeos.

Implementación completa en Python con Flask

Servidor

import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify

app = Flask(__name__)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()


@app.route("/events/<client_id>")
def sse_stream(client_id):
    """SSE endpoint — clients connect here for real-time results."""
    q = queue.Queue()

    with queues_lock:
        client_queues[client_id] = q

    def generate():
        try:
            while True:
                # Block until a result arrives (timeout for keepalive)
                try:
                    data = q.get(timeout=30)
                    yield f"event: captcha-solved\ndata: {data}\n\n"
                except queue.Empty:
                    # Send keepalive comment to prevent connection timeout
                    yield ": keepalive\n\n"
        finally:
            with queues_lock:
                client_queues.pop(client_id, None)

    return Response(
        generate(),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )


@app.route("/submit", methods=["POST"])
def submit_captcha():
    """Submit a CAPTCHA task with callback to this server."""
    data = request.json
    client_id = data["client_id"]
    sitekey = data["sitekey"]
    pageurl = data["pageurl"]

    callback_url = f"{request.host_url}callback?client_id={client_id}"

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    result = resp.json()

    if result.get("status") == 1:
        return jsonify({"task_id": result["request"]})
    return jsonify({"error": result.get("request")}), 400


@app.route("/callback")
def captcha_callback():
    """Receive CaptchaAI callback and push to SSE stream."""
    client_id = request.args.get("client_id")
    task_id = request.args.get("id")
    solution = request.args.get("code")

    import json
    message = json.dumps({
        "task_id": task_id,
        "solution": solution
    })

    with queues_lock:
        q = client_queues.get(client_id)
        if q:
            q.put(message)

    return "OK", 200


if __name__ == "__main__":
    app.run(port=5000, threaded=True)

El timeout=30 del queue.get emite un : keepalive que sostiene la conexión ante proxies impacientes, y X-Accel-Buffering: no desactiva el buffering de nginx.

Cliente del navegador

<!DOCTYPE html>
<html>
<body>
  <button onclick="submitCaptcha()">Solve CAPTCHA</button>
  <div id="results"></div>

  <script>
    const clientId = crypto.randomUUID();
    const resultsDiv = document.getElementById("results");

    // Connect SSE stream
    const eventSource = new EventSource(`/events/${clientId}`);

    eventSource.addEventListener("captcha-solved", (event) => {
      const data = JSON.parse(event.data);
      resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
    });

    eventSource.onerror = () => {
      console.log("SSE connection lost, reconnecting...");
    };

    async function submitCaptcha() {
      const response = await fetch("/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: clientId,
          sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
          pageurl: "https://example.com"
        })
      });
      const result = await response.json();
      resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
    }
  </script>
</body>
</html>

Implementación completa en JavaScript con Express

Servidor

const express = require("express");
const axios = require("axios");

const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";

// Per-client SSE connections: clientId -> Response object
const clients = new Map();

// SSE endpoint
app.get("/events/:clientId", (req, res) => {
  const clientId = req.params.clientId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  });

  clients.set(clientId, res);

  // Keepalive every 30 seconds
  const keepalive = setInterval(() => {
    res.write(": keepalive\n\n");
  }, 30000);

  req.on("close", () => {
    clearInterval(keepalive);
    clients.delete(clientId);
  });
});

// Submit CAPTCHA
app.post("/submit", async (req, res) => {
  const { client_id, sitekey, pageurl } = req.body;
  const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;

  try {
    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) {
      return res.json({ task_id: resp.data.request });
    }
    res.status(400).json({ error: resp.data.request });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
  const clientId = req.query.client_id;
  const taskId = req.query.id;
  const solution = req.query.code;

  const clientRes = clients.get(clientId);
  if (clientRes) {
    const data = JSON.stringify({ task_id: taskId, solution: solution });
    clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log("SSE server running on :3000"));

Consideraciones para producción

Escalado con varias instancias de servidor

  • Las conexiones SSE tienen estado: con varias instancias tras un balanceador, el callback puede aterrizar en una instancia distinta de la que sostiene la conexión del cliente, y el resultado se pierde.
  • Solución: usa Redis Pub/Sub. El callback publica en un canal por cliente y el generador SSE se suscribe a ese mismo canal.
# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))

# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
    if msg["type"] == "message":
        yield f"data: {msg['data'].decode()}\n\n"

Límites de conexión y concurrencia

  • Los navegadores permiten 6 conexiones SSE por dominio en HTTP/1.1; usa HTTP/2 o multiplexa varias tareas por una conexión.
  • La concurrencia real la marcan tus threads en CaptchaAI, no SSE: el plan BASIC ($15/mes) incluye 5 threads con resoluciones ilimitadas por thread, y los planes superiores escalan desde ahí.

Solución de problemas

  • La conexión se cae cada 30 segundos: timeout del proxy o del balanceador; envía keepalives y sube el timeout del proxy.
  • Los resultados no llegan: el callback aterriza en otra instancia; añade Redis Pub/Sub entre el callback y los manejadores SSE.
  • Errores en la consola del navegador: faltan cabeceras CORS; añade Access-Control-Allow-Origin al endpoint SSE.
  • Reconexiones repetidas: SSE mal formado; asegúrate de que \n\n cierra cada evento.

Preguntas frecuentes

¿Qué diferencia hay entre usar SSE y un webhook para recibir resultados de CAPTCHA?

El webhook (el callback de CaptchaAI) es servidor a servidor: la API avisa a tu backend. SSE es el tramo siguiente, de tu servidor al navegador. Se combinan: uno recibe la solución y el otro la reenvía a la interfaz.

¿Qué pasa si el cliente pierde la conexión mientras se resuelve el CAPTCHA?

El EventSource reconecta automáticamente, sin código extra. Lo delicado es el resultado que llega durante la caída:

  • deja que el callback lo deposite en una cola o en Redis, no directamente en la conexión;
  • el cliente lo recibe al reconectar, sin perder ninguna solución.

¿SSE reduce mi consumo de threads en CaptchaAI?

  • No directamente: los threads se consumen mientras un CAPTCHA está en resolución, recibas el resultado como lo recibas.
  • SSE solo elimina el sondeo repetido de res.php, que gasta solicitudes y latencia, no threads.
  • La conexión es agnóstica al tipo: sirve para reCAPTCHA v2, Cloudflare Turnstile o GeeTest v3.

Empieza a transmitir resultados en tiempo real

Ya tienes el circuito completo. Obtén tu clave API de CaptchaAI y conéctalo a tu canal de callback para transmitir cada resolución en tiempo real.

Guías relacionadas:

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