Pour passer de quelques centaines à plusieurs milliers de résolutions CAPTCHA par heure, la réponse tient en une phrase : placez plusieurs workers derrière un équilibreur de charge et laissez-le répartir le trafic. Un worker unique sature vite : il traite les tâches en série et une panne interrompt tout le pipeline.
Un équilibreur augmente le débit, assure le basculement et vous laisse monter en capacité horizontalement. Ce guide couvre le routage NGINX, le serveur API du worker en Python et Node.js, et l'équilibrage côté client.
Vue d'ensemble de l'architecture
[Scraper 1] ──┐ ┌── [Worker 1] ──→ CaptchaAI API
[Scraper 2] ──┤── [Load Balancer] ──┤── [Worker 2] ──→ CaptchaAI API
[Scraper 3] ──┘ └── [Worker 3] ──→ CaptchaAI API
Chaque worker expose la même API interne (/solve, /health) et relaie les tâches vers CaptchaAI. Côté facturation, le total des tâches simultanées de vos workers ne doit pas dépasser les threads de votre plan : un plan PREMIUM ($170/mois, 100 threads) couvre cinq workers plafonnés à 20 tâches.
Configurer NGINX comme équilibreur
NGINX est le point d'entrée le plus courant. Trois stratégies de répartition couvrent la quasi-totalité des cas.
Round-robin, la valeur par défaut
Sans directive particulière, NGINX distribue les requêtes à tour de rôle. Suffisant quand tous vos workers ont la même capacité.
upstream captcha_workers {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
server {
listen 80;
server_name captcha.internal;
location /solve {
proxy_pass http://captcha_workers;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10s;
proxy_read_timeout 300s; # CAPTCHA solving can take minutes
}
location /health {
proxy_pass http://captcha_workers;
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
}
}
least_conn, recommandé pour la résolution de CAPTCHA
La stratégie à privilégier ici : least_conn route chaque requête vers le worker le moins chargé, idéal quand la durée des tâches est imprévisible. On ajoute un poids pour les workers puissants et des contrôles de santé passifs.
upstream captcha_workers {
least_conn; # Route to worker with fewest active connections
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 weight=2; # Higher capacity worker
# Health checks
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}
Ajouter des workers de secours
Un worker marqué backup ne reçoit du trafic que lorsque les autres sont indisponibles — une capacité de réserve froide, non sollicitée en temps normal.
upstream captcha_workers {
least_conn;
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 backup; # Only used when others are down
}
Le serveur API du worker
Chaque worker est un service HTTP qui plafonne sa concurrence et relaie la tâche vers CaptchaAI. Point clé : /health doit refléter la charge réelle, pas seulement le fait que le processus tourne — un worker à pleine capacité renvoie un 503 pour que l'équilibreur le retire de la rotation.
Python avec Flask
import os
import time
import threading
import requests
from flask import Flask, request, jsonify
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
app = Flask(__name__)
# Track active tasks for load reporting
active_tasks = 0
tasks_lock = threading.Lock()
max_concurrent = int(os.environ.get("MAX_CONCURRENT", "20"))
@app.route("/solve", methods=["POST"])
def solve():
global active_tasks
with tasks_lock:
if active_tasks >= max_concurrent:
return jsonify({"error": "WORKER_AT_CAPACITY"}), 503
active_tasks += 1
try:
data = request.json
result = solve_captcha(data)
return jsonify(result)
finally:
with tasks_lock:
active_tasks -= 1
@app.route("/health")
def health():
with tasks_lock:
load = active_tasks / max_concurrent
return jsonify({
"status": "healthy" if load < 0.9 else "overloaded",
"active_tasks": active_tasks,
"max_concurrent": max_concurrent,
"load_pct": round(load * 100, 1)
}), 200 if load < 0.9 else 503
def solve_captcha(data):
session = requests.Session()
payload = {
"key": API_KEY,
"method": data.get("method", "userrecaptcha"),
"googlekey": data.get("sitekey"),
"pageurl": data.get("pageurl"),
"json": 1
}
if data.get("proxy"):
payload["proxy"] = data["proxy"]
payload["proxytype"] = data.get("proxytype", "HTTP")
resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
result = resp.json()
if result.get("status") != 1:
return {"error": result.get("request")}
captcha_id = result["request"]
for _ in range(60):
time.sleep(5)
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": captcha_id, "json": 1
}).json()
if poll.get("status") == 1:
return {"solution": poll["request"], "captcha_id": captcha_id}
if poll.get("request") != "CAPCHA_NOT_READY":
return {"error": poll.get("request")}
return {"error": "TIMEOUT"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, threaded=True)
JavaScript avec Express
Même logique côté Node.js : concurrence suivie, 503 à saturation, et polling de res.php jusqu'au token.
const express = require("express");
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT || "20", 10);
const PORT = parseInt(process.env.PORT || "8080", 10);
let activeTasks = 0;
const app = express();
app.use(express.json());
app.post("/solve", async (req, res) => {
if (activeTasks >= MAX_CONCURRENT) {
return res.status(503).json({ error: "WORKER_AT_CAPACITY" });
}
activeTasks++;
try {
const result = await solveCaptcha(req.body);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
activeTasks--;
}
});
app.get("/health", (req, res) => {
const load = activeTasks / MAX_CONCURRENT;
const status = load < 0.9 ? "healthy" : "overloaded";
res
.status(load < 0.9 ? 200 : 503)
.json({ status, activeTasks, maxConcurrent: MAX_CONCURRENT, loadPct: Math.round(load * 100) });
});
async function solveCaptcha(data) {
const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: data.method || "userrecaptcha",
googlekey: data.sitekey,
pageurl: data.pageurl,
json: 1,
},
});
if (submitResp.data.status !== 1) {
return { error: submitResp.data.request };
}
const captchaId = submitResp.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (pollResp.data.status === 1) {
return { solution: pollResp.data.request, captchaId };
}
if (pollResp.data.request !== "CAPCHA_NOT_READY") {
return { error: pollResp.data.request };
}
}
return { error: "TIMEOUT" };
}
app.listen(PORT, () => console.log(`Worker listening on port ${PORT}`));
Comparer les stratégies de routage
| Stratégie | Fonctionnement | Cas d'usage |
|---|---|---|
| Round-robin | Rotation séquentielle | Workers de capacité égale |
| least_conn | Vers le worker le moins chargé | Résolution de CAPTCHA (durée de tâche variable) |
| Pondérée (weight) | Proportionnelle au poids attribué | Workers de capacité mixte |
| Hachage IP | Même client, même worker | Affinité de session requise |
| Aléatoire | Sélection au hasard | Charge simple et bien répartie |
Recommandation : retenez least_conn. Les durées de tâche s'étalent de 5 à 120 s ; le round-robin enverrait des requêtes à des workers déjà bloqués sur une tâche longue, d'où une charge déséquilibrée.
Équilibrage de charge côté client
Sans équilibreur externe — par exemple un worker OVHcloud ou Scaleway par région — implémentez le routage dans le client, qui reproduit least_conn : on choisit le worker le moins occupé et on bascule en cas d'échec.
import random
import requests
class ClientLoadBalancer:
def __init__(self, workers):
self.workers = [
{"url": url, "healthy": True, "active": 0}
for url in workers
]
def get_worker(self):
healthy = [w for w in self.workers if w["healthy"]]
if not healthy:
raise Exception("No healthy workers")
return min(healthy, key=lambda w: w["active"])
def solve(self, task):
worker = self.get_worker()
worker["active"] += 1
try:
resp = requests.post(
f"{worker['url']}/solve",
json=task,
timeout=300
)
if resp.status_code == 503:
worker["healthy"] = False
return self.solve(task) # Retry on another worker
return resp.json()
except requests.RequestException:
worker["healthy"] = False
return self.solve(task)
finally:
worker["active"] -= 1
lb = ClientLoadBalancer([
"http://10.0.1.10:8080",
"http://10.0.1.11:8080",
"http://10.0.1.12:8080"
])
result = lb.solve({"sitekey": "6Le-wvkS...", "pageurl": "https://example.com"})
Ce schéma convient au multi-régions — un worker par région, eu-west-3 (Paris) pour l'Europe. Si vous journalisez les IP pour la supervision, pensez à vos obligations RGPD.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| 502 Bad Gateway | Le worker a planté ou n'a pas démarré | Consultez les logs du worker ; vérifiez la liaison du port |
| Charge mal répartie | Round-robin avec des durées de tâche variables | Passez à least_conn |
| Contrôle de santé faux positif | Le check passe mais le worker est à pleine capacité | Renvoyez le pourcentage de charge dans la réponse /health |
| Délai de connexion dépassé | proxy_read_timeout trop court |
Réglez-le à 300 s ou plus pour la résolution de CAPTCHA |
FAQ
Quelle stratégie de routage privilégier pour la résolution de CAPTCHA ?
least_conn. Comme la durée d'une résolution varie fortement, c'est la seule stratégie qui tient compte de la charge réelle de chaque worker au lieu de router à l'aveugle.
Pourquoi faut-il régler proxy_read_timeout à 300 s ?
Parce qu'une résolution prend plusieurs dizaines de secondes : le worker interroge res.php en boucle jusqu'au token. Avec la valeur par défaut de NGINX, l'équilibreur couperait la connexion avant la réponse et provoquerait des timeouts côté client.
Faut-il activer des sessions persistantes (sticky sessions) ?
Non. Les requêtes de résolution sont sans état : n'importe quel worker traite n'importe quelle tâche. Des sessions persistantes forceraient un client toujours vers le même worker et recréeraient le déséquilibre de charge que l'on cherche à éviter.
Articles connexes
- Gérer les erreurs de callback CaptchaAI
- Modèles avancés Puppeteer avec CaptchaAI
- Modèles d'architecture pour la résolution à fort volume
Prochaines étapes
Montez en débit : récupérez votre clé API CaptchaAI et déployez vos workers derrière un équilibreur de charge.
Guides associés :
- Basculement haute disponibilité
- Architecture multi-régions
- Dimensionnement du pool de connexions