Le plafond de débit le plus utile n'est pas celui que votre fournisseur vous impose : c'est celui que vous vous imposez. Rien n'empêche une boucle de retry mal écrite d'envoyer des milliers de tâches pendant la nuit, ni deux équipes de saturer la même clé API le même matin. La parade tient en trois limiteurs côté client, posés devant vos appels à in.php : token bucket, fenêtre glissante et plafond budgétaire. Le code Python et Node.js ci-dessous est réutilisable tel quel avec CaptchaAI.
Ce qu'un plafond côté client protège vraiment
| Situation | Sans plafond | Avec plafond |
|---|---|---|
| Une boucle de retry part en vrille après un déploiement | vos threads restent saturés pendant des heures | l'envoi s'arrête au plafond configuré |
| Une agence lyonnaise partage une clé API entre cinq projets clients | consommation impossible à attribuer | quota par projet, refacturation interne possible |
| Le site cible bloque au-delà de 100 req/min | vos comptes de test sont bloqués | le débit reste sous le seuil |
| Le budget interne est fixé à $50/mois | dépassement possible en un après-midi | arrêt net à la limite |
À garder en tête avant d'écrire la moindre ligne : CaptchaAI facture des threads simultanés, pas des résolutions. BASIC ($15/mois, 5 threads) inclut des résolutions illimitées, comme tous les paliers jusqu'à VIP-3 ($7,500/mois, 5 000 threads). Un limiteur ne réduit donc pas la facture : il protège votre capacité en threads, vos comptes cibles et la lisibilité de votre consommation.
Choisir le modèle avant d'écrire le code
| Modèle | À retenir quand | Complexité |
|---|---|---|
| Token bucket | vous tolérez de courtes rafales mais tenez un débit moyen | moyenne |
| Fenêtre glissante | il vous faut un simple compteur de requêtes par intervalle | faible |
| Plafond budgétaire | la contrainte est financière ou contractuelle, pas technique | faible |
| Débit + budget combinés | production multi-équipes, plusieurs clients sur une même clé | moyenne |
- Démarrez avec la fenêtre glissante : c'est le modèle qui s'explique le plus vite en revue de code.
- Passez au token bucket quand vos pics légitimes se font refuser.
- Ajoutez le plafond budgétaire si la contrainte vient d'un engagement client.
Modèle 1 : token bucket en Python pour absorber les rafales
Le token bucket sépare le débit moyen de la taille de rafale autorisée. Les tokens se réapprovisionnent à un rythme fixe, chaque requête en consomme un, et le seau ne dépasse jamais sa capacité. Réglez rate sur le débit soutenable et capacity sur la rafale que vous tolérez d'un coup.
# token_bucket_solver.py
import os
import time
import threading
import requests
API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")
class TokenBucket:
"""Token bucket rate limiter."""
def __init__(self, rate, capacity):
"""
rate: tokens added per second
capacity: max tokens (burst size)
"""
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self, timeout=30):
"""Wait for a token. Returns True if acquired, False on timeout."""
deadline = time.monotonic() + timeout
while True:
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
if time.monotonic() >= deadline:
return False
time.sleep(0.1)
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now
# Allow 10 solves/minute with burst of 5
limiter = TokenBucket(rate=10/60, capacity=5)
def solve_rate_limited(sitekey, pageurl):
"""Solve with rate limiting."""
if not limiter.acquire(timeout=60):
raise Exception("Rate limit: could not acquire token within 60s")
session = requests.Session()
resp = session.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
raise Exception(f"Submit failed: {result.get('request')}")
task_id = result["request"]
time.sleep(15)
for _ in range(25):
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
})
poll_result = poll.json()
if poll_result.get("status") == 1:
return poll_result["request"]
if poll_result.get("request") != "CAPCHA_NOT_READY":
raise Exception(f"Error: {poll_result.get('request')}")
time.sleep(5)
raise Exception("Timeout")
Le verrou threading.Lock rend le limiteur sûr entre threads d'un même processus, pas entre processus : sur plusieurs conteneurs, voyez la section sur les workers distribués plus bas.
Modèle 2 : fenêtre glissante en Node.js pour un quota lisible
La fenêtre glissante compte les envois sur un intervalle mobile et refuse le suivant tant que le plus ancien n'est pas sorti de la fenêtre. Pas de tolérance en rafale, mais un comportement immédiatement compréhensible : « vingt résolutions par tranche de cinq minutes » se vérifie d'un coup d'œil dans les logs.
// sliding_window_solver.js
const axios = require('axios');
const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
class SlidingWindowLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.timestamps = [];
}
async acquire(timeoutMs = 60000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
// Remove expired timestamps
const cutoff = Date.now() - this.windowMs;
this.timestamps = this.timestamps.filter(t => t > cutoff);
if (this.timestamps.length < this.maxRequests) {
this.timestamps.push(Date.now());
return true;
}
// Wait until the oldest request exits the window
const waitMs = Math.min(
this.timestamps[0] + this.windowMs - Date.now() + 10,
deadline - Date.now()
);
if (waitMs > 0) await new Promise(r => setTimeout(r, waitMs));
}
return false;
}
}
// Allow 20 solves per 5 minutes
const limiter = new SlidingWindowLimiter(20, 5 * 60 * 1000);
async function solveRateLimited(sitekey, pageurl) {
const acquired = await limiter.acquire(60000);
if (!acquired) throw new Error('Rate limit exceeded');
const submit = await axios.get('https://ocr.captchaai.com/in.php', {
params: {
key: API_KEY, method: 'userrecaptcha',
googlekey: sitekey, pageurl, json: '1',
},
});
if (submit.data.status !== 1) throw new Error(submit.data.request);
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 25; i++) {
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Timeout');
}
Modèle 3 : plafond budgétaire quotidien
Le troisième modèle raisonne en argent plutôt qu'en requêtes. La constante cost_per_solve n'est pas un tarif CaptchaAI — la facturation reste au thread — mais votre coût interne modélisé, celui que vous refacturez à un client. Ajustez-la à votre méthode de calcul, puis fixez le budget quotidien.
# budget_limiter.py
import os
import time
from datetime import date
class BudgetLimiter:
"""Limit daily CAPTCHA spending."""
def __init__(self, daily_budget, cost_per_solve=0.003):
self.daily_budget = daily_budget
self.cost_per_solve = cost_per_solve
self.daily_spend = 0.0
self.current_date = date.today()
def can_solve(self):
"""Check if budget allows another solve."""
if date.today() != self.current_date:
self.daily_spend = 0.0
self.current_date = date.today()
return self.daily_spend + self.cost_per_solve <= self.daily_budget
def record_solve(self):
"""Record a successful solve against the budget."""
self.daily_spend += self.cost_per_solve
@property
def remaining_budget(self):
return max(0, self.daily_budget - self.daily_spend)
@property
def remaining_solves(self):
return int(self.remaining_budget / self.cost_per_solve)
# $5/day budget
budget = BudgetLimiter(daily_budget=5.00, cost_per_solve=0.003)
def solve_with_budget(sitekey, pageurl):
if not budget.can_solve():
raise Exception(
f"Daily budget exhausted. Remaining: ${budget.remaining_budget:.2f}"
)
# ... solve logic ...
token = "..." # actual solve
budget.record_solve()
return token
Le compteur vit en mémoire : un redémarrage remet les dépenses à zéro. Persistez daily_spend et current_date dans Redis ou en base, sinon un déploiement en milieu de journée double silencieusement votre plafond.
Répartir la limite entre plusieurs workers
Dès que vos workers tournent sur plusieurs instances — deux machines OVHcloud, un groupe de conteneurs Scaleway en eu-west-3 — un compteur local ne limite plus rien : chaque réplica applique le plafond pour lui seul.
- Déplacez l'état dans Redis, une seule clé par palier de limitation.
- Rendez l'incrément atomique :
INCR+EXPIRE, ou un script Lua pour un token bucket exact. - Faites expirer la clé sur la durée de la fenêtre, jamais à l'infini.
Côté journalisation, notez le compteur et la décision, pas les payloads. Les pages protégées par un CAPTCHA sont souvent des formulaires de connexion : conserver leur contenu dans vos logs crée une collecte de données personnelles inutile au regard du RGPD.
Dépannage
| Problème | Cause probable | Correctif |
|---|---|---|
| Toutes les requêtes attendent, aucune ne part | débit trop bas face à la demande réelle | augmentez rate ou élargissez la fenêtre |
| Le plafond budgétaire se réinitialise en pleine journée | processus redémarré, ou horloge système décalée | persistez les dépenses hors du processus |
| Le seau se vide dès la première rafale | capacity trop petite pour le workflow |
augmentez la capacité, gardez le même rate |
| Le limiteur bloque aussi le polling | limiteur appliqué à toutes les requêtes | ne limitez que les envois, jamais l'interrogation |
| Le plafond est dépassé malgré le limiteur | plusieurs réplicas avec chacun leur compteur | passez à un compteur partagé dans Redis |
FAQ
Quelle valeur de débit choisir au démarrage ?
Partez du débit que le site cible tolère, pas de celui que votre plan autorise. Mesurez votre volume réel sur une journée, posez le plafond 20 à 30 % au-dessus du pic observé, puis resserrez.
Faut-il aussi limiter les appels à res.php ?
Non. Limitez uniquement l'envoi (in.php), qui crée les tâches. L'interrogation du résultat ne crée rien de nouveau et la brider ne fait qu'allonger le temps de résolution perçu.
Le rate limiting côté client remplace-t-il un plan avec plus de threads ?
Non, les deux problèmes sont distincts. Le nombre de threads fixe votre parallélisme maximal ; le limiteur décide de ne pas l'utiliser entièrement. Des files d'attente qui s'allongent en permanence, sans incident, signalent un manque de capacité.
Comment le limiteur survit-il à un redémarrage du worker ?
Seul l'état persisté survit. Token bucket et fenêtre glissante repartent à plein, ce qui reste acceptable pour de courtes rafales ; le plafond budgétaire, lui, doit être stocké dans Redis ou en base.
Que renvoyer à l'appelant quand la limite est atteinte ?
Une erreur explicite et typée, jamais un échec silencieux. Distinguez « plafond de débit atteint » de « budget épuisé » : la première se retente avec un backoff exponentiel, la seconde exige une décision humaine.
Articles connexes
Guides associés
- Limites de débit et régulation du trafic
- Passer à 10 000 tâches par heure
- Vérifier le solde et automatiser la recharge
Prochaines étapes
Posez vos propres plafonds avant que la production ne les découvre pour vous — récupérez votre clé API CaptchaAI.