Tutorials

Ratenbegrenzung Ihrer eigenen CAPTCHA-Lösungsanfragen

Drosseln müssen Sie genau eine Stelle: die Einreichung neuer Aufgaben an in.php. Das Polling auf res.php bleibt ungebremst. Dafür genügen drei selbst geschriebene Muster: ein Token-Bucket für gleichmäßigen Takt mit Burst-Reserve, ein Sliding-Window-Zähler für harte Obergrenzen pro Zeitfenster und ein Budget-Limiter als Notbremse für den Tag.

Der Auslöser ist meist derselbe: Ein nächtlicher Crawler auf einem Hetzner-Worker läuft nach einem Deploy in eine Wiederholungsschleife, der GitLab-CI-Runner startet den Job parallel gleich dreimal – und am Morgen stehen deutlich mehr Aufgaben im Log als geplant. CaptchaAI verkraftet diese Parallelität; Ihr Zielsystem und Ihre Kapazitätsplanung oft nicht.

Was das Drosseln eigener CAPTCHA-Anfragen begrenzt

CaptchaAI rechnet Thread-basiert ab: Ein Thread ist eine gleichzeitig laufende Lösung; die Lösungen pro Thread sind im Abrechnungsmonat unbegrenzt. BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Aufgaben, ADVANCE (90 $/Monat, 50 Threads) entsprechend fünfzig. Ein eigener Limiter senkt also nicht die Rechnung – er steuert, wie schnell Ihr Code Aufgaben nachschiebt. In der Praxis ist das der wichtigere Hebel:

  • Ein Thread bleibt belegt, solange eine Lösung läuft. Wer ungebremst einreicht, wartet nur länger in der eigenen Warteschlange.
  • Das Zielsystem hat sein eigenes Rate-Limit. Wird dort ab 100 Anfragen pro Minute gedrosselt, hilft kein schnellerer Solver.
  • Fehler skalieren mit. Eine Wiederholungsschleife ohne Obergrenze produziert stundenlang Aufgaben, bevor sie auffällt.
  • Mehrere Teams teilen sich einen API-Schlüssel. Ohne Absprache belegt ein Lasttest die Threads des Produktions-Jobs.

Welches Muster zu welchem Fall passt

Muster Geeignet für Aufwand
Token-Bucket gleichmäßige Rate mit erlaubten Spitzen mittel
Sliding Window feste Anzahl Anfragen pro Zeitfenster gering
Budget-Limiter Kostendeckel pro Tag, Woche oder Monat gering
Rate und Budget kombiniert Produktivsysteme mit mehreren Jobs mittel

Für die meisten Teams ist die Kombination richtig: Der Token-Bucket regelt den Takt, der Budget-Limiter zieht im Fehlerfall die Reißleine.

Muster 1: Token-Bucket für gleichmäßigen Takt

Ein Token-Bucket füllt sich mit fester Rate auf; jede Einreichung verbraucht ein Token, bei leerem Bucket wartet der Aufruf. Der Vorteil gegenüber einem starren Zähler: Angesparte Tokens erlauben kurze Spitzen, ohne den Durchschnitt zu heben.

Token-Bucket in 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")

rate=10/60 steht für zehn Lösungen pro Minute, capacity=5 erlaubt fünf davon als Burst. Beide Werte gehören in die Konfiguration, nicht fest in den Code. Wichtig ist die Sperre über threading.Lock: Ohne sie verzählen sich parallele Threads am selben Bucket, und das Limit greift nicht.

Muster 2: Sliding Window für harte Obergrenzen

Der Sliding-Window-Zähler merkt sich die Zeitstempel der letzten Einreichungen und lässt eine neue nur zu, wenn im laufenden Fenster Platz ist. Er kennt keine Burst-Reserve, bildet dafür Vorgaben wie „höchstens 20 Anfragen in fünf Minuten“ exakt ab.

Sliding Window in 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');
}

Beachten Sie die berechnete Wartezeit: Statt in einer engen Schleife zu prüfen, wartet der Limiter genau so lange, bis der älteste Zeitstempel aus dem Fenster fällt. Das spart CPU-Zeit und macht das Verhalten unter Last vorhersehbar.

Muster 3: Budget-Limiter als Tagesobergrenze

Der dritte Limiter zählt keine Anfragen, sondern Kosten – und stoppt beim Tagesbudget.

# 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

Der Wert cost_per_solve ist Ihr eigener Rechenwert, kein Listenpreis. Da CaptchaAI pro Thread abrechnet und die Lösungen pro Thread unbegrenzt sind, ergibt sich der effektive Preis je Lösung aus dem Monatspreis geteilt durch das erwartete Volumen. Setzen Sie ihn konservativ an; Preise gelten in US-Dollar.

Zwei Details entscheiden über die Praxistauglichkeit: Der Tageszähler muss einen Neustart überstehen (Datei, Redis oder Datenbank statt Prozessspeicher), und das Zurücksetzen hängt an einer festen Zeitzone – bei Jobs kurz nach Mitternacht ist der Wechsel zwischen MEZ und MESZ ein realer Fehlerfall.

Rechenbeispiel: nächtlicher Crawler mit fester Obergrenze

Ein Preis-Crawler soll zwischen 2 und 6 Uhr rund 4.000 Produktseiten prüfen; etwa jede zehnte zeigt eine reCAPTCHA-v2-Abfrage. Das sind ungefähr 400 Lösungen in vier Stunden, also rund 1,7 pro Minute. Mit ADVANCE (90 $/Monat, 50 Threads) ist die Parallelität kein Engpass – der Engpass ist das Zielsystem, das ab etwa 100 Anfragen pro Minute drosselt.

Tragfähige Konfiguration: Token-Bucket mit drei Lösungen pro Minute (Reserve für Wiederholungen), capacity bei fünf, dazu ein Budget-Limiter, der bei 800 Lösungen pro Nacht stoppt – dem Doppelten des Erwartungswerts. Läuft der Job in eine Schleife, endet er nach dem doppelten Soll statt nach vier Stunden Dauerlast.

Fallen personenbezogene Daten an, gilt die übliche DSGVO-Sorgfalt: IP-Adressen zählen dazu; Rechtsgrundlage und Aufbewahrungsfristen prüfen Sie unabhängig vom Solver.

CAPTCHA-Anfragen über mehrere Worker gemeinsam drosseln

Alle drei Muster halten ihren Zustand im Prozessspeicher. Sobald zwei Container, zwei GitLab-CI-Runner oder zwei Kubernetes-Pods parallel laufen, multipliziert sich das Limit mit der Anzahl der Prozesse. Zwei Wege haben sich bewährt:

  • Zentraler Zähler in Redis. Der Limiter liest und schreibt atomar (INCR mit TTL oder ein kleines Lua-Skript); Bibliotheken wie rate-limiter-flexible für Node.js bringen das mit.
  • Ein dedizierter Solver-Worker. Alle Jobs legen Aufgaben in eine Warteschlange, nur ein Prozess spricht mit der API und trägt den Limiter.

In kleineren Setups ist die zweite Variante meist die ruhigere: ein Ort für Limit, Logging und Fehlerbehandlung.

Fehlerbehebung

Problem Ursache Lösung
Alle Anfragen stehen in der Warteschlange, keine läuft Rate zu niedrig für die tatsächliche Last Rate oder Fenstergröße anheben und den Durchsatz erneut messen
Der Bucket ist nach wenigen Sekunden leer capacity zu klein für den Zuschnitt des Jobs Burst-Kapazität erhöhen oder Aufgaben zeitlich verteilen
Auch das Polling wird gedrosselt Der Limiter umschließt den gesamten Aufruf statt nur die Einreichung Nur in.php drosseln und res.php frei abfragen lassen
Das Tagesbudget setzt sich mitten am Tag zurück Prozess-Neustart oder Wechsel der Systemzeit Tagesverbrauch persistieren und die Zeitzone fest verdrahten
Token geliefert, vom Ziel aber abgelehnt Sitekey, Page-URL oder Sitzungskontext passen nicht Parameter neu erfassen und das Token in derselben Sitzung eintragen

Häufige Fragen

Wie hängt ein eigener Limiter mit den Threads meines Plans zusammen?

Er ersetzt sie nicht. Die Threads bestimmen, wie viele Lösungen gleichzeitig laufen; Ihr Limiter bestimmt, wie viele Aufgaben Ihr Code pro Zeiteinheit nachreicht. Sinnvoll ist ein Limit knapp unterhalb Ihrer Thread-Zahl.

Welche Startwerte sind für rate und capacity sinnvoll?

Thread-Zahl geteilt durch die typische Lösungszeit ergibt die theoretisch mögliche Rate. Starten Sie bei etwa 70 % davon und setzen Sie capacity auf den Bedarf einer Minute – danach messen statt schätzen.

Bleibt ein Token gültig, wenn der Limiter die Anfrage verzögert?

Nicht beliebig lange: reCAPTCHA-Tokens sind in der Regel rund zwei Minuten gültig. Drosseln Sie deshalb vor der Einreichung, nicht zwischen fertiger Lösung und dem Absenden des Formulars.

Zählt ein fehlgeschlagener Versuch gegen mein Limit?

Ja. Der Limiter zählt beim Einreichen, unabhängig vom Ergebnis – auch Fehlversuche erzeugen Last. Sollen Wiederholungen bevorzugt durchkommen, geben Sie ihnen einen eigenen kleinen Bucket.

Lohnt sich das auch bei kleinen Volumina?

Bei einem einzelnen Job mit wenigen Threads reicht oft eine Semaphore für gleichzeitige Aufrufe. Sobald mehrere Jobs, Zeitpläne oder Teams denselben API-Schlüssel nutzen, ist ein expliziter Limiter der günstigere Weg.

Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.