Tutorials

Server-Sent Events for Real-Time CAPTCHA Solve Notifications

Polling res.php every 5 seconds works, but it wastes requests and adds latency. Server-Sent Events (SSE) let your server push CAPTCHA solutions to connected clients the instant they arrive — zero wasted requests, sub-second delivery.

How SSE Fits the CAPTCHA Workflow

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. Client connects to your SSE endpoint (persistent HTTP connection)
  2. Client submits a CAPTCHA task to CaptchaAI with pingback pointing to your server
  3. CaptchaAI solves and sends the result to your callback endpoint
  4. Your server pushes the result through the SSE stream to the client

Full Implementation — Python (Flask)

Server

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)

Browser Client

<!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>

Full Implementation — JavaScript (Express)

Server

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"));

SSE vs WebSocket vs Polling

Feature SSE WebSocket Polling
Direction Server → Client Bidirectional Client → Server
Protocol HTTP/1.1+ WS/WSS HTTP
Auto-reconnect Built-in Manual N/A
Browser support All modern All modern All
Complexity Low Medium Low
Wasted requests None None Many
Best for CAPTCHA results Yes Overkill Works but wasteful

SSE is ideal for CAPTCHA results because data only flows server-to-client.

Production Considerations

Scaling with Multiple Server Instances

SSE connections are stateful — if your server has multiple instances behind a load balancer, the callback might hit a different instance than the one holding the client's SSE connection.

Solution: Use Redis Pub/Sub as a message bus:

# 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"

Connection Limits

Browsers limit SSE connections to 6 per domain (HTTP/1.1). Use HTTP/2 for higher limits, or multiplex multiple task results through a single SSE connection per client.

Troubleshooting

Issue Cause Fix
SSE connection drops every 30s Proxy/load balancer timeout Send keepalive comments; increase proxy timeout
Results don't arrive Callback hitting different server instance Add Redis Pub/Sub between callback and SSE handlers
Browser shows errors in console CORS headers missing Add Access-Control-Allow-Origin header to SSE endpoint
Multiple reconnections Server sending malformed SSE Ensure \n\n terminates each event; validate data format

FAQ

Does SSE work behind Cloudflare?

Yes, but Cloudflare may buffer responses. Disable response buffering with the X-Accel-Buffering: no header, or use Cloudflare's streaming mode.

How many concurrent SSE connections can one server handle?

Node.js handles 10,000+ concurrent SSE connections easily since each is a lightweight kept-alive HTTP connection. Python with threading is more limited — use an async framework (FastAPI with asyncio) for high concurrency.

Should I use SSE for non-browser clients?

For CLI tools or backend services, direct callback handling or queue-based approaches are simpler. SSE is most useful when pushing results to browser-based dashboards or web applications.

Next Steps

Stream CAPTCHA solutions in real-time — get your CaptchaAI API key and connect SSE to your callback pipeline.

Related guides:

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.