Deux réglages suffisent pour maîtriser un pipeline de résolution : une capacité, la rafale que vous tolérez, et un taux de recharge, le débit soutenu en requêtes par seconde. C'est tout ce qu'est un token bucket, et c'est le premier correctif à appliquer quand ERROR_TOO_MUCH_REQUESTS apparaît dans vos logs. Un ThreadPoolExecutor à 30 workers borne le nombre de tâches en vol, jamais la vitesse à laquelle elles frappent l'endpoint.
Deux réglages, rien de plus
| Réglage | Ce qu'il contrôle | Comment le fixer |
|---|---|---|
| Capacité | La rafale maximale absorbée d'un coup | 2 × le taux de recharge |
| Taux de recharge | Le débit soutenu, en requêtes par seconde | Ce que l'API accepte sans erreur |
| Bucket vide | La requête attend | Rien n'est rejeté, tout est étalé |
Ce que fait le bucket, seconde par seconde
[Bucket] capacity=20, refill=10/sec
Time 0: ████████████████████ 20 tokens available
→ 15 requests consume 15 tokens
Time 0: █████ 5 tokens remain
Time 1s: ███████████████ 15 tokens (5 + 10 refilled)
→ 15 requests consume 15 tokens
Time 1s: (empty) 0 tokens
Time 2s: ██████████ 10 tokens (0 + 10 refilled)
→ Request waits if bucket is empty
Token bucket, leaky bucket ou fenêtre glissante ?
| Algorithme | Comportement | Cas d'usage typique |
|---|---|---|
| Token bucket | Débit lissé, rafales tolérées | Appels d'API CAPTCHA |
| Leaky bucket | Débit de sortie fixe, aucune rafale | Quotas stricts |
| Fenêtre fixe | Comptage par fenêtre, pics en bordure | Compteurs simples |
| Fenêtre glissante | Comptage sur période glissante | Application fine du quota |
Pour un scraper, le token bucket s'impose : votre crawler découvre vingt CAPTCHA d'un coup.
Un token bucket thread-safe en Python
import time
import threading
class TokenBucket:
def __init__(self, capacity, refill_rate):
"""
Args:
capacity: Maximum tokens (burst size)
refill_rate: Tokens added per second
"""
self.capacity = capacity
self.refill_rate = refill_rate
self.tokens = capacity
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self, timeout=None):
"""Block until a token is available."""
deadline = time.monotonic() + timeout if timeout else float("inf")
while True:
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
# Check timeout
if time.monotonic() >= deadline:
return False
# Wait before retrying (avoid busy loop)
time.sleep(min(1.0 / self.refill_rate, 0.1))
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
new_tokens = elapsed * self.refill_rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill = now
Le verrou protège tokens et last_refill ; la recharge se calcule à la demande, sans thread de fond ni dérive d'horloge.
Brancher le limiteur sur l'API CaptchaAI
Un seul acquire() avant la soumission suffit. L'interrogation des résultats reste hors du limiteur : elle est légère et déjà espacée de cinq secondes.
import os
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Allow 10 submissions/sec with burst of 20
rate_limiter = TokenBucket(capacity=20, refill_rate=10)
def solve_captcha_rate_limited(sitekey, pageurl):
"""Solve with rate limiting on submission."""
# Wait for token before submitting
rate_limiter.acquire()
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
raise RuntimeError(data.get("request"))
captcha_id = data["request"]
# Polling doesn't need rate limiting (separate concern)
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(result.get("request"))
raise TimeoutError("Solve timeout")
# Run 100 tasks through rate limiter
tasks = [
{"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": f"https://example.com/p/{i}"}
for i in range(100)
]
with ThreadPoolExecutor(max_workers=30) as executor:
futures = {
executor.submit(
solve_captcha_rate_limited, t["sitekey"], t["pageurl"]
): t for t in tasks
}
for future in as_completed(futures):
task = futures[future]
try:
solution = future.result()
print(f"[OK] {task['pageurl']}")
except Exception as e:
print(f"[ERR] {task['pageurl']}: {e}")
La même logique en JavaScript
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.refillRate = refillRate; // tokens per second
this.tokens = capacity;
this.lastRefill = Date.now();
this.waitQueue = [];
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
async acquire() {
this._refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
// Wait until a token is available
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
this._refill();
this.tokens -= 1;
}
}
La version asynchrone calcule le temps d'attente exact au lieu de boucler : l'event loop de Node.js reste libre.
Traiter un lot de cent tâches
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const rateLimiter = new TokenBucket(20, 10); // 20 burst, 10/sec sustained
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveCaptchaLimited(sitekey, pageurl) {
// Wait for rate limit token
await rateLimiter.acquire();
const submitResp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
},
}
);
if (submitResp.data.status !== 1) {
throw new Error(submitResp.data.request);
}
const captchaId = submitResp.data.request;
for (let i = 0; i < 60; i++) {
await sleep(5000);
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (result.data.status === 1) return result.data.request;
if (result.data.request !== "CAPCHA_NOT_READY") {
throw new Error(result.data.request);
}
}
throw new Error("TIMEOUT");
}
// Solve 100 tasks — rate limiter ensures max 10 submissions/sec
async function batchSolve(tasks) {
const results = await Promise.allSettled(
tasks.map((t) => solveCaptchaLimited(t.sitekey, t.pageurl))
);
const solved = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
console.log(`Solved: ${solved}, Failed: ${failed}`);
}
Calibrer la capacité et le taux de recharge
| Charge de travail | Capacité (rafale) | Taux de recharge (soutenu) |
|---|---|---|
| Scraping léger | 5 | 2/sec |
| Automatisation standard | 20 | 10/sec |
| Pipeline à fort volume | 50 | 30/sec |
| Débit maximal | 100 | 50/sec |
Règles empiriques
- Capacité = 2 × le taux de recharge : deux secondes de rafale absorbées.
- Démarrez bas, montez par paliers en surveillant le taux d'erreur.
- Limitez les soumissions uniquement, jamais l'interrogation des résultats.
Aligner le débit sur vos threads
La facturation CaptchaAI se fait au thread simultané, pas à la résolution. Avec BASIC ($15/mois, 5 threads), jamais plus de cinq résolutions en vol : un taux de recharge de 2/sec suffit. Sur ADVANCE ($90/mois, 50 threads), visez la ligne « automatisation standard » ci-dessus.
Le piège du déploiement distribué
Dès que les workers tournent sur une flotte Scaleway ou OVHcloud, ou sur trois instances en eu-west-3 (Paris), chaque processus applique son propre bucket en mémoire : le débit réel est multiplié d'autant. Divisez le taux cible par le nombre de processus, ou déportez les compteurs dans Redis.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Les requêtes sont encore limitées | Débit supérieur à ce que l'API accepte | Baisser le taux de recharge |
| Latence anormale à la soumission | Bucket vide, attente de recharge | Augmenter la capacité pour absorber les rafales |
| Mémoire qui grimpe | La file d'attente s'accumule sans borne | Fixer une taille maximale et refuser le surplus |
| Limiteur non partagé entre processus | Compteurs en mémoire locale | Passer sur un token bucket adossé à Redis |
FAQ
Le token bucket remplace-t-il un pool de threads ?
Non, les deux couches sont complémentaires : le pool borne les tâches simultanées, le bucket la vitesse d'entrée. Sans lui, trente workers envoient trente soumissions dans la même milliseconde.
Quelle capacité choisir selon mon plan ?
Partez du nombre de threads de votre plan : les résolutions en vol sont déjà plafonnées par ce quota. Le bucket sert à lisser les pics de soumission qui déclenchent ERROR_TOO_MUCH_REQUESTS.
Comment partager un limiteur entre plusieurs machines ?
Déplacez l'état dans Redis et incrémentez les compteurs via un script Lua : recharge et consommation restent atomiques. Un bucket par clé API suffit.
Prochaines étapes
Passez d'un débit subi à un débit choisi : récupérez votre clé API CaptchaAI et instrumentez vos soumissions dès le premier lot.