Tutoriales

Cómo limitar tus propias solicitudes de resolución de CAPTCHA

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:

Los comentarios están deshabilitados para este artículo.