El límite que más te va a doler no es el de CaptchaAI: es el que tú no pusiste. La plataforma acepta alta concurrencia sin quejarse, así que un bucle mal cerrado seguirá enviando tareas hasta que alguien mire el panel de control. El freno práctico va en tu cliente, antes de que la solicitud salga hacia in.php. Aquí implementas tres patrones — token bucket, ventana deslizante y tope de presupuesto — con código que puedes pegar hoy en tu pipeline de CaptchaAI.
Cuándo conviene ponerte un límite a ti mismo
Cuatro situaciones en las que el freno del lado del cliente se paga solo:
| Escenario | Sin límite | Con límite |
|---|---|---|
| Un bug provoca un bucle infinito de resolución | Consume el saldo entero | Se detiene en el tope configurado |
| Varios equipos comparten una misma clave API | Gasto descoordinado | Reparto justo por equipo |
| El sitio de destino bloquea por encima de 100 req/min | Las cuentas acaban bloqueadas | Te mantienes por debajo del umbral |
| Presupuesto mensual acotado | Se puede agotar en una tarde | Tope duro aplicado |
Un caso que se repite en la región: una agencia en Ciudad de México monitoriza precios de catálogo para tres clientes con una sola cuenta CaptchaAI en el plan ADVANCE ($90/mes, 50 threads). Cuando el crawler de un cliente entra en reintentos, se come los threads de los otros dos. Eso no se arregla subiendo de plan, sino dando a cada cliente su propio limitador con una cuota acordada. Ten presente el modelo: CaptchaAI cobra por thread concurrente, con resoluciones ilimitadas dentro del plan, así que tu límite no ahorra por resolución: ordena el reparto y protege al sitio de destino.
Si hay web scraping de por medio, respeta los términos del sitio y la normativa de protección de datos aplicable (GDPR y LOPDGDD en España, LFPDPPP en México).
Patrón 1: token bucket para ráfagas controladas
El token bucket es el patrón por defecto cuando tu carga es irregular. El depósito se rellena a una tasa fija y cada envío consume un token: si hay tokens acumulados puedes permitirte una ráfaga corta, y al vaciarse la frecuencia de solicitudes vuelve al promedio configurado.
Los parámetros que importan son rate (tokens por segundo) y capacity (tamaño de la ráfaga). Empieza con una capacidad igual a los threads de tu plan y ajústala midiendo, no adivinando.
Token bucket en Python
# 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")
Un detalle que importa en producción: acquire() lleva su propio timeout. Si el depósito lleva más de 60 segundos vacío, la llamada falla rápido en lugar de dejar hilos colgados; así la cola no se convierte en una fuga de memoria.
Patrón 2: ventana deslizante cuando solo importa el recuento
Si solo necesitas cumplir una regla del tipo "como máximo 20 envíos cada 5 minutos", el token bucket resulta innecesariamente sofisticado. La ventana deslizante guarda las marcas de tiempo recientes, descarta las que salen de la ventana y bloquea al llegar al máximo: pierdes el control fino de ráfaga y ganas mucho menos código que razonar.
Ventana deslizante en JavaScript
// 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');
}
Este limitador vive en la memoria del proceso: con cuatro workers tendrás cuatro ventanas independientes y, en la práctica, cuatro veces el límite que creías tener. La salida para varios procesos está en las preguntas frecuentes.
Patrón 3: tope de presupuesto diario
El tercer patrón no cuenta solicitudes: cuenta dinero. Fijas un presupuesto diario, registras cada resolución completada y cortas cuando se agota. Es el freno adecuado cuando el riesgo que cubres es contable — un cliente que se pasa de su cuota, un entorno de pruebas que no debería gastar.
# 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
El cost_per_solve del ejemplo es un coste interno que defines tú para repartir el gasto entre clientes o proyectos: los planes de CaptchaAI se facturan por threads, no por resolución. Nunca es un precio de lista.
Cómo elegir entre los tres
| Patrón | Mejor para | Complejidad |
|---|---|---|
| Token bucket | Tasa media suave con margen de ráfaga | Media |
| Ventana deslizante | Recuento simple de solicitudes por ventana | Baja |
| Tope de presupuesto | Control de gasto por día, semana o mes | Baja |
| Combinado (tasa + presupuesto) | Sistemas en producción | Media |
La mayoría de los equipos combina dos, ortogonales entre sí: un token bucket que protege al sitio de destino y un tope de presupuesto que protege la factura.
Problemas frecuentes al aplicarlos
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Todo se queda en cola y nada se ejecuta | La tasa configurada es menor que la demanda real | Sube rate o amplía la ventana |
| El presupuesto se reinicia a media jornada | Cambio de hora del sistema o reinicio del proceso | Persiste el gasto diario en Redis o en base de datos |
| El depósito se vacía en la primera ráfaga | capacity demasiado baja para el lote |
Sube la capacidad al número de threads de tu plan |
| El limitador frena también los sondeos | El limitador envuelve todas las llamadas | Aplícalo solo a los envíos a in.php |
| El límite real es el doble del configurado | Varios workers con limitadores en memoria | Pasa a un limitador compartido en Redis |
Preguntas frecuentes
¿El limitador va sobre in.php o también sobre res.php?
Solo sobre in.php. El sondeo a res.php no crea tareas nuevas ni ocupa threads adicionales: frenarlo solo alarga el tiempo de resolución sin ahorrarte nada.
¿Qué tasa debería configurar al empezar?
Parte del número de threads de tu plan, no de una cifra inventada. Con BASIC ($15/mes, 5 threads) no tiene sentido permitir 60 envíos por minuto: nunca habrá más de cinco CAPTCHA en vuelo y el resto solo engorda tu cola.
¿Cómo comparto un mismo límite entre varios procesos o contenedores?
Con un limitador respaldado por Redis y operaciones atómicas. rate-limiter-flexible en Node.js o un contador con INCR y EXPIRE desde Python funcionan bien; lo importante es que el estado viva fuera del proceso.
¿Limitarme a mí mismo empeora mi tasa de éxito?
No, si limitas solo los envíos. Lo que sí mejora es la estabilidad frente al sitio de destino, que suele bloquear por frecuencia de solicitudes antes que por volumen acumulado.
¿Qué pasa con las tareas que el limitador rechaza?
Decídelo explícitamente: o las encolas para reintentarlas, o las descartas registrando el motivo. Dejar que la excepción suba sin control hasta el worker es lo peor: pierdes la tarea y no queda rastro de cuántas rechazaste.
Artículos relacionados
Próximos pasos
Pon el freno en tu cliente antes de subir de plan: obtén tu clave API de CaptchaAI y mide una semana con estos contadores.
Guías relacionadas: