CAPTCHA рано или поздно не решится — не из-за бага в вашем коде, а потому что тайм-ауты, обнулившийся баланс или ограничение частоты запросов встречаются в любом достаточно долго работающем конвейере. Вопрос не «случится ли сбой», а что делает автоматизация в этот момент: падает целиком или продолжает работу в ограниченном режиме.
Плавная деградация — это набор шаблонов, которые превращают единичный отказ в решении CAPTCHA в управляемое событие: пропустить задачу, поставить её в очередь на повтор либо временно снизить нагрузку на API — вместо того чтобы терять весь прогресс пайплайна из-за одной неудачной попытки.
Какие сбои случаются при вызове API CaptchaAI
Прежде чем проектировать восстановление, разделите отказы на три группы — у каждой своя логика реакции:
- Временные — тайм-аут, ограничение частоты запросов: имеет смысл повторить попытку.
- Постоянные — плохие параметры, неверный sitekey: повтор не поможет, нужно чинить источник данных.
- Инфраструктурные — нулевой баланс, обрыв соединения: нужна пауза и оповещение, а не мгновенный повтор.
Полная карта отказов и кодов ошибок:
| Отказ | Код ошибки | Стратегия восстановления |
|---|---|---|
| Тайм-аут | CAPCHA_NOT_READY (превышено число опросов) |
Повторить попытку с новым запросом |
| Плохие параметры | ERROR_BAD_PARAMETERS |
Записать в лог и пропустить — исправить извлечение данных |
| Неправильный sitekey | ERROR_WRONG_GOOGLEKEY |
Повторно извлечь sitekey со страницы |
| Нулевой баланс | ERROR_ZERO_BALANCE |
Пауза, оповещение, ожидание пополнения |
| Превышена частота запросов | ERROR_TOO_MUCH_REQUESTS |
Экспоненциальная задержка (backoff) |
| API недоступен | Ошибка соединения | Circuit breaker + повтор |
Схема 1: пропустить сбой и продолжить пакетную обработку
Для пакетных операций, где отдельные неудачи не критичны для итогового результата, проще всего пропустить проблемную позицию и двигаться дальше:
Когда применять
- Для больших пакетов URL, где потеря пары адресов не влияет на общий результат.
- Когда цена ошибки ниже цены попытки исправить её прямо в моменте.
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_or_skip(captcha_type, sitekey, page_url, max_retries=2):
"""Try to solve; return None on failure instead of crashing."""
for attempt in range(max_retries):
try:
token = solve_captcha(captcha_type, sitekey, page_url)
if token:
return token
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
return None # Skip this item
def process_urls(urls):
results = []
skipped = []
for url in urls:
sitekey = extract_sitekey(url)
if not sitekey:
skipped.append({"url": url, "reason": "no_sitekey"})
continue
token = solve_or_skip("recaptcha_v2", sitekey, url)
if token:
data = submit_form(url, token)
results.append({"url": url, "data": data})
else:
skipped.append({"url": url, "reason": "solve_failed"})
print(f"Processed: {len(results)}, Skipped: {len(skipped)}")
return results, skipped
Схема 2: очередь повторных попыток
Если задачу нельзя просто выбросить, отправьте её в очередь и обработайте позже — с экспоненциальной задержкой между попытками, чтобы не биться в одну и ту же временную проблему без пауз:
Когда применять
- Когда у каждой задачи есть ценность и пропускать её нельзя.
- Когда сбой похож на временный — тайм-аут или ограничение частоты запросов.
from collections import deque
import json
class RetryQueue:
def __init__(self, max_retries=3, backoff_base=60):
self.queue = deque()
self.max_retries = max_retries
self.backoff_base = backoff_base
def add(self, task):
task["retry_count"] = task.get("retry_count", 0) + 1
if task["retry_count"] <= self.max_retries:
task["retry_after"] = time.time() + (
self.backoff_base * task["retry_count"]
)
self.queue.append(task)
return True
return False # Exceeded max retries
def get_ready(self):
"""Get tasks ready for retry."""
ready = []
remaining = deque()
now = time.time()
while self.queue:
task = self.queue.popleft()
if task["retry_after"] <= now:
ready.append(task)
else:
remaining.append(task)
self.queue = remaining
return ready
def save(self, filepath="retry_queue.json"):
with open(filepath, "w") as f:
json.dump(list(self.queue), f)
def load(self, filepath="retry_queue.json"):
try:
with open(filepath) as f:
self.queue = deque(json.load(f))
except FileNotFoundError:
pass
# Usage
retry_q = RetryQueue()
def process_with_retry(task):
try:
token = solve_captcha(task["type"], task["sitekey"], task["url"])
if token:
return submit_form(task["url"], token)
else:
retry_q.add(task)
except Exception:
retry_q.add(task)
# Process retry queue periodically
def drain_retry_queue():
ready = retry_q.get_ready()
for task in ready:
process_with_retry(task)
Схема 3: деградированный режим при полном отказе API
Когда сервис решения перестаёт отвечать вовсе, точечных повторов уже недостаточно — конвейеру нужен третий уровень защиты: временный ограниченный режим работы.
Когда применять
Используйте эту схему, когда обычных повторов уже недостаточно — сервис решения либо перегружен, либо временно недоступен целиком, и продолжать долбить его новыми запросами только продлевает восстановление.
class CaptchaSolver:
def __init__(self, api_key):
self.api_key = api_key
self.degraded = False
self.failure_count = 0
self.failure_threshold = 5
self.recovery_time = None
def solve(self, captcha_type, sitekey, page_url):
if self.degraded:
if time.time() < self.recovery_time:
return self._degraded_action(page_url)
else:
self.degraded = False
self.failure_count = 0
try:
token = self._solve_api(captcha_type, sitekey, page_url)
self.failure_count = 0
return token
except Exception as e:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self._enter_degraded_mode()
raise
def _enter_degraded_mode(self):
self.degraded = True
self.recovery_time = time.time() + 300 # 5 min
print("Entering degraded mode for 5 minutes")
# Send alert
def _degraded_action(self, url):
"""What to do when solving is unavailable."""
# Option A: Skip CAPTCHA pages entirely
return None
# Option B: Queue for later
# retry_queue.add({"url": url, ...})
# return None
# Option C: Try alternative solver
# return self._solve_with_backup_api(...)
def _solve_api(self, captcha_type, sitekey, page_url):
# Normal CaptchaAI API call
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
for _ in range(24):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": "1"
}).json()
if result["status"] == 1:
return result["request"]
if result["request"] != "CAPCHA_NOT_READY":
raise Exception(result["request"])
raise Exception("TIMEOUT")
Комбинированный сценарий на Node.js
Тот же принцип, собранный в одном классе на Node.js: счётчик отказов, автоматическое восстановление по таймеру и очередь повторов:
class ResilientSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.retryQueue = [];
this.failureCount = 0;
this.degraded = false;
}
async solve(type, sitekey, pageUrl) {
if (this.degraded) {
this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
return null;
}
try {
const token = await this._callApi(type, sitekey, pageUrl);
this.failureCount = 0;
return token;
} catch (err) {
this.failureCount++;
if (err.message === 'ERROR_ZERO_BALANCE') {
this._enterDegraded(600000); // 10 min
return null;
}
if (this.failureCount >= 5) {
this._enterDegraded(300000); // 5 min
}
this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
return null;
}
}
_enterDegraded(durationMs) {
this.degraded = true;
console.warn(`Degraded mode for ${durationMs / 1000}s`);
setTimeout(() => {
this.degraded = false;
this.failureCount = 0;
this.drainRetryQueue();
}, durationMs);
}
async drainRetryQueue() {
const tasks = this.retryQueue.splice(0);
for (const task of tasks) {
await this.solve(task.type, task.sitekey, task.pageUrl);
}
}
async _callApi(type, sitekey, pageUrl) {
// Standard submit + poll
const axios = require('axios');
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: this.apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl: pageUrl, json: 1 },
});
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: this.apiKey, action: 'get', id: taskId, json: 1 },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error('TIMEOUT');
}
}
Что важно в этом примере
ERROR_ZERO_BALANCEпереводит воркер в более долгий деградированный режим (10 минут), чем прочие сбои (5 минут) — пополнение баланса обычно занимает больше времени, чем сетевой сбой.- Очередь повторов дренируется автоматически по таймеру при выходе из деградированного режима — отдельный планировщик не нужен.
Типичные проблемы при внедрении
На практике эти три схемы иногда работают не так, как задумано. Вот частые симптомы и их настоящие причины:
| Проблема | Причина | Исправление |
|---|---|---|
| Все задачи пропускаются | Деградированный режим срабатывает слишком агрессивно | Поднять порог отказов (failure_threshold) |
| Очередь повторов растёт бесконечно | Задачи никогда не решаются успешно | Ограничить число повторов; перенести в очередь недоставленных сообщений |
| Восстановление слишком медленное | Слишком длинный тайм-аут деградированного режима | Сократить время восстановления; добавить проверку работоспособности API |
| Очередь теряется при перезапуске | Очередь хранится только в памяти процесса | Сохранять очередь в файл или базу данных |
Совет: прежде чем менять пороги в коде, проверьте, не совпадает ли всплеск отказов по времени с плановым обслуживанием API или сетевыми проблемами на стороне вашего хостинга — иногда «баг» в логике деградации на самом деле внешний сбой.
Матрица решений: повторять, воспроизводить или пропускать
- Повторяйте попытку только тогда, когда сбой выглядит временным, а состояние сессии на стороне сайта ещё не устарело.
- Воспроизводите шаг заново, если запрос можно детерминированно пересобрать — без повторной отправки формы и без риска задвоить действие пользователя.
- Пропускайте задачу или ставьте конвейер на паузу, если очередная попытка лишь спишет поток впустую и не увеличит шанс на успех.
Для команд, чьи воркеры крутятся в европейских дата-центрах или разворачиваются ближе к Казахстану и Центральной Азии, нестабильное соединение через мобильные или резидентные прокси — обычное дело: короткий сетевой тайм-аут легко спутать с реальным отказом решения на стороне API. Поэтому порог входа в деградированный режим стоит подбирать с запасом на практике, а не копировать значение по умолчанию из документации. И ещё один практический момент: если вы логируете пропущенные задачи вместе с URL и метаданными формы, храните в логе только то, что действительно нужно для повторной попытки — это упрощает соответствие 152-ФЗ «О персональных данных» для команд, работающих с российской аудиторией, и остаётся хорошей практикой в духе GDPR для остальных.
Часто задаваемые вопросы
Чем плавная деградация отличается от circuit breaker?
Circuit breaker — это бинарный переключатель: он полностью блокирует вызовы API, как только видит слишком много отказов подряд. Плавная деградация шире — это стратегия поведения всего конвейера при сбое: пропустить задачу, поставить её в очередь или переключиться на резервный сценарий. На практике оба шаблона работают вместе: выключатель решает, когда останавливать вызовы, а деградация — что делать с задачами, пока он разомкнут.
Нужно ли повторять попытку при любой ошибке API?
Нет. ERROR_BAD_PARAMETERS и ERROR_WRONG_GOOGLEKEY означают, что запрос собран неправильно — повтор с теми же параметрами провалится точно так же. Повторяйте только временные сбои: тайм-ауты, ERROR_TOO_MUCH_REQUESTS, обрывы соединения. Остальное — записывайте в лог и чините источник данных, обычно это неправильно извлечённый sitekey.
Сколько раз стоит повторять задачу, прежде чем сдаться?
Для большинства пайплайнов достаточно 2–3 попыток с экспоненциальной задержкой — как backoff_base, умноженный на номер попытки, в примере RetryQueue выше. Если после третьей попытки задача всё ещё не решена, дешевле переместить её в очередь недоставленных сообщений и разобраться отдельно, чем бесконечно жечь потоки на заведомо проблемной задаче.
Как не терять очередь повторов при перезапуске сервиса?
In-memory очередь на deque, как в примере выше, исчезает при перезапуске процесса. В продакшене сохраняйте очередь во внешнее хранилище — файл, Redis или таблицу в базе данных — и восстанавливайте её при старте методом load(). Это особенно важно, если деградированный режим включается на несколько минут: за это время в очереди может накопиться заметный объём задач.
Что делать, если баланс потоков закончился в середине рабочего дня?
ERROR_ZERO_BALANCE — не повод для повтора: пока баланс не пополнен, попытки будут проваливаться одинаково. Ставьте конвейер на паузу, отправляйте оповещение ответственному и переходите в деградированный режим до подтверждения пополнения — как показано в примере на Node.js, где именно эта ошибка сразу переводит воркер в degraded на более длинный интервал, чем остальные сбои.
Постройте отказоустойчивую автоматизацию CAPTCHA на CaptchaAI
Получите API-ключ на captchaai.com и добавьте в конвейер очередь повторов и деградированный режим уже сегодня.