¿Necesitas enviar cientos de CAPTCHA a la API sin disparar ERROR_TOO_MUCH_REQUESTS? La respuesta práctica es un token bucket: fijas una tasa sostenida de envíos —por ejemplo, 10 por segundo— y dejas margen para ráfagas cortas cuando la cola se llena de golpe. Así conviertes una concurrencia caótica en un ritmo predecible, con un saldo de API que no se evapora y un costo mensual que sí puedes planificar.
Cómo funciona un token bucket
La idea es sencilla: un depósito arranca lleno de tokens, cada solicitud consume uno y el depósito se rellena a una tasa constante. Mientras haya tokens, las solicitudes salen sin esperar; cuando se agotan, esperan a la siguiente recarga.
[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
Tres propiedades definen su comportamiento:
- Capacity: el tamaño máximo de ráfaga que absorbe de golpe.
- Refill rate: las solicitudes sostenidas por segundo a largo plazo.
- Cuando el depósito está vacío, las solicitudes esperan (no se rechazan; solo se limitan).
Frente a un simple sleep fijo entre solicitudes, la ventaja es clara: el token bucket separa las dos dimensiones —cuántas solicitudes caben de golpe (capacity) y cuántas se sostienen por segundo (refill_rate)—, así que absorbe los picos sin romper el límite medio.
Token bucket en Python
Token bucket seguro para hilos
Esta versión usa un Lock para que varios hilos de trabajo compartan el mismo depósito sin condiciones de carrera. La recarga se calcula por tiempo transcurrido, así que no hace falta un temporizador en segundo plano.
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
Resolver CAPTCHA con límite de tasa
Cada tarea llama a rate_limiter.acquire() antes de enviar. El ThreadPoolExecutor lanza 30 hilos, pero el token bucket mantiene como máximo 10 envíos por segundo hacia CaptchaAI.
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}")
Fíjate en que el sondeo (res.php) queda fuera del límite: es liviano y ya se autolimita con time.sleep(5). Solo los envíos (in.php) pasan por el depósito.
Token bucket en JavaScript
Token bucket asíncrono
En Node.js no hay hilos ni locks: la versión asíncrona calcula el tiempo de espera y lo resuelve con una promesa. El resultado es el mismo comportamiento de recarga por tiempo transcurrido.
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;
}
}
Resolución de lotes con límite de tasa
Con Promise.allSettled lanzas las 100 tareas a la vez, pero el await rateLimiter.acquire() dentro de cada una impone la tasa. Ninguna promesa rechazada tumba al lote: el recuento final separa las resueltas de las fallidas.
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}`);
}
Cómo elegir capacity y refill_rate
No hay un único valor correcto: depende del volumen y de lo tolerante que sea tu pipeline a las ráfagas. Estos puntos de partida cubren la mayoría de los casos:
| Carga de trabajo | Capacity (ráfaga) | Refill rate (sostenida) |
|---|---|---|
| scraping ligero | 5 | 2/sec |
| Automatización estándar | 20 | 10/sec |
| Pipeline de alto volumen | 50 | 30/sec |
| Rendimiento máximo | 100 | 50/sec |
Reglas prácticas:
- Fija la capacity en 2 × el refill_rate (permite ráfagas de dos segundos).
- Empieza conservador y sube mientras vigilas la tasa de errores.
- Limita solo los envíos: el sondeo es liviano y se autolimita.
Un ejemplo concreto: una agencia en México o Argentina que monitoriza precios en marketplaces regionales (del tipo MercadoLibre o Amazon.es) para el QA de sus propios flujos suele encontrar los CAPTCHA en ráfagas, cuando el crawler entra en una categoría entera de golpe. Con capacity=20 y refill_rate=10 absorbe esos picos sin saturar la API, y como CaptchaAI se factura en USD por threads —desde BASIC ($15/mes, 5 threads)—, el costo mensual queda previsible aunque la moneda local fluctúe. Respeta siempre los términos de servicio y la normativa de protección de datos aplicable.
Token bucket frente a otros algoritmos
Hay cuatro algoritmos clásicos de límite de tasa. Esta comparación aclara por qué el token bucket encaja tan bien con las llamadas a la API de CAPTCHA:
| Algoritmo | Comportamiento | Lo mejor para |
|---|---|---|
| Token bucket | Tasa suave con margen para ráfagas | Llamadas a la API de CAPTCHA |
| Leaky bucket | Tasa de salida fija, sin ráfagas | Requisitos de tasa estrictos |
| Ventana fija | Conteo por ventana de tiempo, ráfagas en los bordes | Contadores simples |
| Ventana deslizante | Conteo sobre un periodo continuo | Control preciso de la tasa |
El token bucket es la opción por defecto más sólida: permite las ráfagas naturales (tu scraper encuentra 20 CAPTCHA a la vez) sin renunciar a una tasa sostenida bajo control.
Solución de problemas
| Problema | Causa | Solución |
|---|---|---|
| Las solicitudes siguen recibiendo throttling | Límite configurado por encima de lo que permite la API | Baja el refill_rate para ajustarlo a los límites de CaptchaAI |
| Latencia alta en las solicitudes | Tokens agotados, esperando la recarga | Aumenta la capacity para escenarios de ráfaga |
| La memoria crece sin parar | La cola de espera se acumula | Fija un tamaño máximo de cola y rechaza las solicitudes que sobren |
| El límite no se comparte entre procesos | Solo vive en memoria | Usa un token bucket basado en Redis para un límite distribuido |
Preguntas frecuentes
¿Qué valores de capacity y refill_rate elijo para empezar?
Empieza conservador: capacity=20 y refill_rate=10 cubren bien la automatización estándar. Sube el refill_rate solo mientras vigilas la tasa de errores. La regla práctica es capacity = 2 × refill_rate, que deja margen para ráfagas de dos segundos.
¿Sigo recibiendo ERROR_TOO_MUCH_REQUESTS aunque limite la tasa?
Tu límite está demasiado alto. Baja el refill_rate. Comprueba además si varios procesos comparten la misma clave API: la tasa se suma entre todos, así que un límite por proceso no basta para el total.
¿Cómo comparto el mismo límite entre varios servidores o workers?
Un token bucket en memoria solo controla un proceso. Para un pipeline distribuido, mueve el estado del depósito a Redis: todos los workers consultan y descuentan tokens del mismo contador, de modo que el límite sea global y no por máquina.
¿El límite de tasa afecta a mi plan o a mis threads de CaptchaAI?
No. El token bucket vive en tu código y solo regula el ritmo de envíos. Tu plan —por ejemplo, BASIC ($15/mes, 5 threads) o ADVANCE ($90/mes, 50 threads)— define cuántos CAPTCHA resuelves en paralelo; ajusta el refill_rate para no superar la capacidad de tus threads.
Artículos relacionados
- Patrón circuit breaker para llamadas a la API de CAPTCHA
Próximos pasos
Monta una resolución de CAPTCHA con la tasa bajo control: obtén tu clave API de CaptchaAI e implementa una frecuencia de envíos sostenible.
Guías relacionadas:
- Cómo limitar tus propias solicitudes
- Límites de tasa y throttling
- Cómo resolver 10.000 tareas por hora