Schlüsselrotation heißt: Vor jeder Anfrage entscheidet Ihre Automatisierung neu, welcher API-Schlüssel den nächsten Auftrag übernimmt, und schaltet weiter, sobald einer kein verwertbares Ergebnis liefert. Sichtbar wird das im Fehlerfall: Meldet ein Konto ERROR_IP_NOT_ALLOWED, weil der CI-Runner eine neue IP hat, übernimmt der nächste Schlüssel – die Pipeline läuft weiter, statt auszufallen.
Dieser Leitfaden zeigt vier bewährte Muster in Python und Node.js – Round-Robin, gewichtete Auswahl nach Guthaben, Failover über Fehlercodes und das Nachziehen der Guthaben. Dazu die Betriebsfragen, die mehr Zeit kosten als der Rotator selbst: Wo liegen die Schlüssel, und wer merkt es, wenn nur noch einer aktiv ist?
Wann sich ein Schlüsselpool lohnt
Ein Pool ist kein Selbstzweck. Er rechnet sich dort, wo ohnehin mehrere Konten existieren oder wo ein Ausfall Sie nicht sofort erreicht:
- Agenturen mit mehreren Mandanten. Ein eigenes Konto je Kunde ist in DACH-Projekten die Regel, schon wegen der Kostenstellen. Der Rotator kapselt die Auswahl.
- Getrennte Umgebungen. Eigene Schlüssel für Staging und Produktion verhindern, dass ein Testlauf das Produktionsguthaben aufbraucht.
- Nachtläufe ohne Bereitschaft. Jobs zwischen 2 und 6 Uhr brauchen ein automatisches Failover, weil niemand eingreift.
- Wartung und Sperren. Fällt ein Konto aus, übernimmt der Rest des Pools.
Nicht sinnvoll ist Rotation als Durchsatz-Trick. CaptchaAI rechnet pro Thread ab, jeder Plan enthält unbegrenzte Lösungen pro Thread – von BASIC (15 $/Monat, 5 Threads) bis VIP-3 (7.500 $/Monat, 5.000 Threads). Mehr Parallelität bekommen Sie über mehr Threads, nicht über mehr Schlüssel.
Die vier Muster im Überblick
| Muster | Wann sinnvoll | Zustand im Speicher |
|---|---|---|
| Round-Robin | gleichwertige Konten | Zähler |
| Gewichtete Auswahl | ungleich gefüllte Guthaben | Guthaben je Schlüssel |
| Failover | Fehlercodes sperren Schlüssel | disabled-Flag |
| Geplantes Nachziehen | Prozesse mit langer Laufzeit | Timer plus Guthaben |
Die Muster schließen einander nicht aus: In der Praxis kombinieren die meisten Teams gewichtete Auswahl mit Failover und lassen im Hintergrund einen Timer die Guthaben aktualisieren.
Muster 1: Round-Robin durch den Schlüsselpool
Die einfachste Variante läuft gleichmäßig durch alle Schlüssel. itertools.cycle genügt für einen einzelnen Prozess; sobald mehrere Worker denselben Iterator nutzen, gehört ein Lock davor – sonst greifen zwei Threads denselben Schlüssel ab.
Python
import itertools
import requests
API_KEYS = [
"KEY_ACCOUNT_1",
"KEY_ACCOUNT_2",
"KEY_ACCOUNT_3",
]
key_cycle = itertools.cycle(API_KEYS)
def get_next_key():
return next(key_cycle)
def solve_captcha(sitekey, page_url):
api_key = get_next_key()
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"[{api_key[:8]}...] {data['request']}")
print(f"Submitted with key {api_key[:8]}...")
return data["request"], api_key
task_id, used_key = solve_captcha("6Le-SITEKEY", "https://example.com")
Der Rückgabewert enthält neben der Task-ID auch den verwendeten Schlüssel – halten Sie diese Zuordnung fest. Beim Abholen des Ergebnisses über res.php muss derselbe Schlüssel mitgehen wie bei der Übermittlung an in.php.
Muster 2: gewichtete Auswahl nach Guthaben
Sobald die Konten unterschiedlich gefüllt sind, wird strikte Gleichverteilung unpraktisch: Das kleinste Guthaben ist zuerst leer, danach scheitert jede dritte Anfrage. Die gewichtete Auswahl fragt das Guthaben je Schlüssel über action=getbalance ab und zieht zufällig, aber proportional zum Restguthaben.
import random
import requests
import threading
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
class KeyRotator:
def __init__(self, keys):
self.keys = {k: {"balance": 0, "failures": 0, "disabled": False} for k in keys}
self._lock = threading.Lock()
self.refresh_balances()
def refresh_balances(self):
for key in self.keys:
try:
resp = requests.get(RESULT_URL, params={
"key": key, "action": "getbalance", "json": "1"
}, timeout=10).json()
if resp["status"] == 1:
self.keys[key]["balance"] = float(resp["request"])
self.keys[key]["disabled"] = False
else:
self.keys[key]["disabled"] = True
except Exception:
self.keys[key]["disabled"] = True
def get_key(self):
with self._lock:
available = {
k: v for k, v in self.keys.items()
if not v["disabled"] and v["balance"] > 0.01
}
if not available:
raise Exception("No API keys with balance available")
# Weighted random by balance
keys = list(available.keys())
weights = [available[k]["balance"] for k in keys]
return random.choices(keys, weights=weights, k=1)[0]
def report_failure(self, key, error_code):
with self._lock:
self.keys[key]["failures"] += 1
if error_code in ("ERROR_WRONG_USER_KEY", "ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE", "ERROR_IP_NOT_ALLOWED"):
self.keys[key]["disabled"] = True
print(f"[rotator] Disabled key {key[:8]}...: {error_code}")
def report_success(self, key, cost=0.003):
with self._lock:
self.keys[key]["balance"] -= cost
self.keys[key]["failures"] = 0
rotator = KeyRotator(["KEY_1", "KEY_2", "KEY_3"])
# Usage
api_key = rotator.get_key()
# ... solve captcha ...
rotator.report_success(api_key)
Zwei Details lohnen einen zweiten Blick. Der Parameter cost in report_success() ist eine lokale Buchhaltungsgröße, damit die Gewichtung zwischen zwei Abfragen aktuell bleibt – eine Preisangabe ist er nicht, denn die CaptchaAI-Pläne rechnen pro Thread ab, nicht pro Lösung. Die Schwelle balance > 0.01 hält Konten aus dem Pool, deren Guthaben für keinen weiteren Auftrag mehr reicht.
Muster 3: Failover über Fehlercodes
Failover trennt zwei Fehlerklassen. Dauerhafte Fehler betreffen den Schlüssel selbst: ERROR_WRONG_USER_KEY, ERROR_KEY_DOES_NOT_EXIST, ERROR_ZERO_BALANCE und ERROR_IP_NOT_ALLOWED. Nur sie rechtfertigen eine Sperre. Timeouts und HTTP-5xx-Antworten sagen nichts über den Schlüssel aus – wer ihn deswegen sperrt, hat nach wenigen Minuten Netzwerkunruhe einen leeren Pool.
Python
def solve_with_failover(sitekey, page_url, max_attempts=3):
for attempt in range(max_attempts):
api_key = rotator.get_key()
try:
resp = requests.post(SUBMIT_URL, data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}, timeout=15)
data = resp.json()
if data["status"] != 1:
rotator.report_failure(api_key, data["request"])
continue
rotator.report_success(api_key)
return data["request"], api_key
except requests.RequestException:
rotator.report_failure(api_key, "NETWORK_ERROR")
continue
raise Exception(f"All {max_attempts} keys failed")
max_attempts bleibt bewusst klein: Bei einem flächendeckenden Ausfall soll der Job mit klarer Fehlermeldung enden, nicht durch einen leeren Pool kreisen.
Node.js
const axios = require('axios');
class KeyRotator {
constructor(keys) {
this.keys = keys.map(k => ({ key: k, disabled: false, failures: 0 }));
this.index = 0;
}
getKey() {
const available = this.keys.filter(k => !k.disabled);
if (available.length === 0) throw new Error('No API keys available');
const entry = available[this.index % available.length];
this.index++;
return entry.key;
}
disable(key, reason) {
const entry = this.keys.find(k => k.key === key);
if (entry) {
entry.disabled = true;
console.log(`[rotator] Disabled ${key.substring(0, 8)}...: ${reason}`);
}
}
}
const rotator = new KeyRotator(['KEY_1', 'KEY_2', 'KEY_3']);
async function solveWithFailover(sitekey, pageurl, maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) {
const apiKey = rotator.getKey();
try {
const resp = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl, json: 1 }
});
if (resp.data.status !== 1) {
rotator.disable(apiKey, resp.data.request);
continue;
}
return { taskId: resp.data.request, apiKey };
} catch (err) {
rotator.disable(apiKey, 'NETWORK_ERROR');
}
}
throw new Error('All keys failed');
}
Schlüssel ablegen statt einprogrammieren
Drei Schlüssel im Quelltext sind drei Geheimnisse im Repository – und bleiben es auch nach dem Löschen, weil die Git-Historie sie behält. Laden Sie die Liste aus einer Umgebungsvariablen:
import os
API_KEYS = os.environ["CAPTCHAAI_KEYS"].split(",")
# Set: CAPTCHAAI_KEYS=key1,key2,key3
rotator = KeyRotator(API_KEYS)
const API_KEYS = process.env.CAPTCHAAI_KEYS.split(',');
const rotator = new KeyRotator(API_KEYS);
In DACH-Teams landen diese Werte typischerweise in maskierten GitLab-CI-Variablen oder in GitHub-Actions-Secrets. Auf einem eigenen Worker – etwa einem VPS bei Hetzner oder netcup – gehören sie in eine EnvironmentFile-Datei des systemd-Dienstes mit den Rechten 600, in Kubernetes in ein Secret. Entscheidend ist die Austauschbarkeit: Ein kompromittierter Schlüssel muss sich mit einem neuen Variablenwert und einem Neustart ersetzen lassen, nicht mit einem Commit. Details dazu im Leitfaden zu Zugangsdaten in Umgebungsvariablen.
Muster 4: Guthaben im laufenden Betrieb nachziehen
Prozesse mit Tagen an Laufzeit rechnen sonst dauerhaft mit den Zahlen vom Start. Ein Daemon-Thread hält sie aktuell:
import threading
def periodic_refresh(rotator, interval=300):
def refresh():
while True:
rotator.refresh_balances()
for key, info in rotator.keys.items():
print(f" {key[:8]}...: ${info['balance']:.2f} "
f"{'(disabled)' if info['disabled'] else '(active)'}")
threading.Event().wait(interval)
t = threading.Thread(target=refresh, daemon=True)
t.start()
periodic_refresh(rotator, interval=300) # every 5 minutes
Fünf Minuten sind ein brauchbarer Startwert: Häufiger belastet res.php ohne Nutzen, seltener bemerkt der Rotator ein leergelaufenes Konto erst nach einer Serie von Fehlversuchen.
Was Sie protokollieren sollten
Ein Rotator versteckt Ausfälle – das ist sein Zweck und zugleich sein Risiko. Ohne Telemetrie fällt der erste Ausfall gar nicht auf, der letzte dafür umso härter. Vier Signale genügen:
- Schlüsselpräfix pro Anfrage. Die ersten acht Zeichen wie in den Beispielen: genug für die Zuordnung, ohne das Geheimnis zu protokollieren.
- Anzahl aktiver Schlüssel als Metrik. Alarm bei weniger als zwei – ab da stoppt der nächste Ausfall die Pipeline.
- Fehlerquote je Konto. Ein Schlüssel, der dauerhaft schlechter läuft, hat meist ein Problem mit der IP-Freigabe.
- Guthabenverlauf je Konto – die Grundlage, um Auffüllungen zu planen statt zu reagieren.
Häufige Fehlerbilder
| Symptom | Ursache | Behebung |
|---|---|---|
| Alle Schlüssel gesperrt | Guthaben überall aufgebraucht | Konten auffüllen, Sperrlogik prüfen |
| Immer derselbe Schlüssel | Round-Robin-Index läuft nicht weiter | Zähler mit einem Lock absichern |
| Schlüssel zu Unrecht gesperrt | Vorübergehender Fehler als dauerhaft behandelt | Nur bei ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE, ERROR_IP_NOT_ALLOWED sperren |
ERROR_IP_NOT_ALLOWED nach Umzug |
Neue Worker-IP nicht freigegeben | IP-Freigabe je Konto nachziehen |
| Ergebnis nicht auffindbar | Abfrage mit anderem Schlüssel als die Übermittlung | Task-ID und Schlüssel gemeinsam speichern |
Häufige Fragen
Erhöht Schlüsselrotation den Durchsatz?
Nur mittelbar. Jedes Konto bringt die Threads seines Plans mit, doch Kapazität über mehrere Konten zu stückeln vervielfacht die Verwaltung: mehrere Guthaben, mehrere IP-Freigaben, mehrere Rechnungen. Für mehr parallele Lösungen ist ein Plan mit mehr Threads der direktere Weg.
Welche Fehlercodes sollten einen Schlüssel dauerhaft sperren?
Genau vier: ERROR_WRONG_USER_KEY, ERROR_KEY_DOES_NOT_EXIST, ERROR_ZERO_BALANCE und ERROR_IP_NOT_ALLOWED. Alles andere – Timeouts, Verbindungsabbrüche, HTTP-5xx-Antworten – ist vorübergehend und gehört in eine Wiederholungslogik mit exponentiellem Backoff, nicht in die Sperrliste.
Funktioniert Rotation zusammen mit IP-Freigaben?
Ja, sofern jedes Konto dieselben Worker-IPs freigibt. Fehlt eine Freigabe, liefert dieser Teil des Pools ERROR_IP_NOT_ALLOWED, und der Rotator sperrt intakte Schlüssel. Bei neuen Workern also erst die Freigaben nachziehen, dann den Pool erweitern.
Wie viele Schlüssel sind sinnvoll?
Zwei genügen für ein einfaches Failover; drei bis fünf verteilen die Last gleichmäßiger, sobald mehr als 1.000 Lösungen pro Tag anfallen. Mehr Schlüssel bringen vor allem mehr Verwaltungsaufwand.
Was tun, wenn der gesamte Pool gesperrt ist?
Den Prozess kontrolliert beenden statt weiter zu versuchen – im Beispielcode wirft get_key() genau dafür eine Exception. Dazu eine Meldung an den Bereitschaftskanal mit dem letzten Fehlercode je Schlüssel; meist steht dort ERROR_ZERO_BALANCE.
Mit dem zweiten Schlüssel anfangen
Zwischen einem stillstehenden Nachtlauf und einer Pipeline, die morgens fertig ist, liegen oft nur ein zweites Konto und zwanzig Zeilen Code. Holen Sie sich Ihren API-Schlüssel auf captchaai.com und legen Sie den Pool gleich mit zwei Schlüsseln an.