API Tutorials

Реализация надежной логики повторных попыток с помощью CaptchaAI API

Запрос к in.php вернул HTTP 429 или временный ERROR_NO_SLOT_AVAILABLE? Это не повод останавливать пайплайн вручную. Такие сбои почти всегда закрываются одним точным повтором с задержкой — а не перезапуском скрипта руками. Если каждая сетевая заминка требует вмешательства человека, проблема не в CaptchaAI API, а в отсутствующей retry-логике на вашей стороне.

Дальше — рабочая схема повторов: какие ошибки стоит повторять, а какие нет, экспоненциальная задержка с джиттером, устойчивый опрос res.php и circuit breaker, который останавливает поток запросов, если сервис действительно недоступен. Код готов к продакшену — его можно вставить в существующий воркер без переписывания.


Какие ошибки CaptchaAI API стоит повторять

Не каждая ошибка одинакова. Часть — временные всплески нагрузки на очередь решения, часть — постоянные проблемы конфигурации, которые повтор не исправит и только впустую сожжёт лимит попыток. Разделите их сразу:

Ошибка Повторять? Почему
ERROR_NO_SLOT_AVAILABLE ✅ Да Временно заполнена очередь
HTTP 429 ✅ Да Ограничение частоты запросов
HTTP 500/502/503 ✅ Да Временная ошибка сервера
Тайм-аут соединения ✅ Да Сбой сети
CAPCHA_NOT_READY ✅ Продолжайте опрос Задача ещё обрабатывается
ERROR_WRONG_USER_KEY ❌ Нет Ошибка конфигурации — исправьте ключ
ERROR_KEY_DOES_NOT_EXIST ❌ Нет Неверный API-ключ
ERROR_ZERO_BALANCE ❌ Нет Сначала пополните баланс
ERROR_CAPTCHA_UNSOLVABLE ⚠️ Пересоздайте задачу Тот же ID не даст другого результата — нужен новый запрос

Постоянные ошибки (ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE) повторять бессмысленно — задача не станет ближе к решению, только вырастет число неудачных запросов в логах. CaptchaAI тарифицирует потоки, а не количество попыток, так что сами по себе повторы не увеличивают счёт. Но поток, застрявший в бесконечных ретраях постоянной ошибки, дольше остаётся занятым и снижает эффективную пропускную способность в рамках вашего тарифа. На плане BASIC ($15/мес, 5 потоков) это особенно заметно: лишние повторы буквально забирают слот у следующей задачи в очереди.


Базовая retry-логика с экспоненциальной задержкой

Идея простая: при временной ошибке — подождать, попробовать снова, и с каждой следующей попыткой ждать дольше. Так параллельные клиенты не бьют по API одновременно после общего сбоя (проблема «грозового стада»). Ниже — реализация на Python с разделением ошибок на постоянные и временные и экспоненциальной задержкой с джиттером:

import requests
import time
import random

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://ocr.captchaai.com"

# Errors that should NOT be retried
PERMANENT_ERRORS = {
    "ERROR_WRONG_USER_KEY",
    "ERROR_KEY_DOES_NOT_EXIST",
    "ERROR_ZERO_BALANCE",
    "ERROR_BAD_PARAMETERS",
    "ERROR_WRONG_CAPTCHA_ID",
}

# Errors that should be retried
TRANSIENT_ERRORS = {
    "ERROR_NO_SLOT_AVAILABLE",
    "ERROR_TOO_MUCH_REQUESTS",
}


def submit_with_retry(method, max_retries=5, **params):
    """Submit task with retry on transient errors."""
    data = {"key": API_KEY, "method": method, "json": 1}
    data.update(params)

    for attempt in range(max_retries):
        try:
            resp = requests.post(
                f"{BASE_URL}/in.php", data=data, timeout=30,
            )

            # HTTP-level errors
            if resp.status_code in (429, 500, 502, 503):
                wait = _backoff(attempt)
                print(f"HTTP {resp.status_code}, retry in {wait:.1f}s")
                time.sleep(wait)
                continue

            result = resp.json()

            # Permanent errors — don't retry
            if result.get("request") in PERMANENT_ERRORS:
                raise RuntimeError(f"Permanent error: {result['request']}")

            # Transient errors — retry
            if result.get("request") in TRANSIENT_ERRORS:
                wait = _backoff(attempt)
                print(f"{result['request']}, retry in {wait:.1f}s")
                time.sleep(wait)
                continue

            # Success
            if result.get("status") == 1:
                return result["request"]

            # Unknown error
            raise RuntimeError(f"Unknown error: {result.get('request')}")

        except requests.ConnectionError:
            wait = _backoff(attempt)
            print(f"Connection error, retry in {wait:.1f}s")
            time.sleep(wait)

        except requests.Timeout:
            wait = _backoff(attempt)
            print(f"Timeout, retry in {wait:.1f}s")
            time.sleep(wait)

    raise RuntimeError(f"Failed after {max_retries} retries")


def _backoff(attempt, base=2, max_wait=60):
    """Exponential backoff with jitter."""
    wait = min(base ** attempt, max_wait)
    jitter = random.uniform(0, wait * 0.5)
    return wait + jitter

Функция _backoff ограничивает максимальное ожидание 60 секундами и добавляет случайный джиттер до 50% — без него все клиенты, упавшие в одну и ту же секунду, повторят запрос синхронно и снова получат 429. Пяти попыток отправки в большинстве интеграций достаточно: если и пятая не проходит, проблема почти наверняка не в перегрузке очереди, а в конфигурации.


Опрос результата с обработкой сбоев

Отправка задачи — только половина цикла. После in.php нужно опрашивать res.php, пока статус не станет 1, и здесь сетевые сбои встречаются чаще, чем при отправке: опрос идёт каждые несколько секунд, поэтому кратковременная просадка соединения — обычное дело, особенно при нестабильном мобильном интернете или трансграничном трафике из региона СНГ до дата-центра. poll_with_retry считает подряд идущие сбои отдельно от статуса задачи и прерывается только тогда, когда их накопилось слишком много:

def poll_with_retry(task_id, timeout=120, max_poll_errors=3):
    """Poll for result with error retry."""
    start = time.time()
    consecutive_errors = 0

    while time.time() - start < timeout:
        time.sleep(5)

        try:
            resp = requests.get(f"{BASE_URL}/res.php", params={
                "key": API_KEY, "action": "get",
                "id": task_id, "json": 1,
            }, timeout=15)

            if resp.status_code in (429, 500, 502, 503):
                consecutive_errors += 1
                if consecutive_errors >= max_poll_errors:
                    raise RuntimeError("Too many poll errors")
                time.sleep(_backoff(consecutive_errors))
                continue

            data = resp.json()
            consecutive_errors = 0  # Reset on success

            if data["request"] == "CAPCHA_NOT_READY":
                continue

            if data["request"] in PERMANENT_ERRORS:
                raise RuntimeError(f"Solve error: {data['request']}")

            return data["request"]

        except (requests.ConnectionError, requests.Timeout):
            consecutive_errors += 1
            if consecutive_errors >= max_poll_errors:
                raise RuntimeError("Too many poll connection errors")
            time.sleep(_backoff(consecutive_errors))

    raise TimeoutError(f"Task {task_id} timeout after {timeout}s")

Счётчик consecutive_errors обнуляется при каждом успешном ответе — это важно: одна случайная просадка канала не должна суммироваться с ошибками, случившимися пятью минутами раньше. Если ваша инфраструктура регулярно работает через нестабильные каналы (мобильные прокси, дата-центры в Центральной Азии или Восточной Европе с более длинным RTT до ocr.captchaai.com), поднимите max_poll_errors до 5–7 вместо стандартных трёх — так вы не будете терять задачи из-за кратковременных всплесков задержки.


Класс-решатель с полной retry-логикой

Разнесённые по функциям примеры выше удобны для объяснения, но в реальном сервисе логику отправки, опроса и статистики стоит собрать в один объект — так проще логировать долю успешных решений и вовремя замечать деградацию:

class RetrySolver:
    """Production-grade solver with comprehensive retry logic."""

    def __init__(self, api_key, max_submit_retries=5, max_poll_retries=3,
                 poll_timeout=120):
        self.api_key = api_key
        self.base = "https://ocr.captchaai.com"
        self.max_submit_retries = max_submit_retries
        self.max_poll_retries = max_poll_retries
        self.poll_timeout = poll_timeout
        self.stats = {
            "total": 0, "success": 0, "retry": 0,
            "permanent_error": 0, "timeout": 0,
        }

    def solve(self, method, **params):
        self.stats["total"] += 1

        # Submit with retry
        task_id = self._submit(method, **params)

        # Poll with retry
        try:
            token = self._poll(task_id)
            self.stats["success"] += 1
            return token
        except TimeoutError:
            self.stats["timeout"] += 1
            raise

    def _submit(self, method, **params):
        data = {"key": self.api_key, "method": method, "json": 1}
        data.update(params)

        for attempt in range(self.max_submit_retries):
            try:
                resp = requests.post(
                    f"{self.base}/in.php", data=data, timeout=30,
                )

                if resp.status_code in (429, 500, 502, 503):
                    self.stats["retry"] += 1
                    time.sleep(_backoff(attempt))
                    continue

                result = resp.json()

                if result.get("request") in PERMANENT_ERRORS:
                    self.stats["permanent_error"] += 1
                    raise RuntimeError(f"Permanent: {result['request']}")

                if result.get("request") in TRANSIENT_ERRORS:
                    self.stats["retry"] += 1
                    time.sleep(_backoff(attempt))
                    continue

                if result.get("status") == 1:
                    return result["request"]

            except (requests.ConnectionError, requests.Timeout):
                self.stats["retry"] += 1
                time.sleep(_backoff(attempt))

        raise RuntimeError("Submit failed after retries")

    def _poll(self, task_id):
        start = time.time()
        errors = 0

        while time.time() - start < self.poll_timeout:
            time.sleep(5)
            try:
                resp = requests.get(f"{self.base}/res.php", params={
                    "key": self.api_key, "action": "get",
                    "id": task_id, "json": 1,
                }, timeout=15)

                if resp.status_code in (429, 500, 502, 503):
                    errors += 1
                    if errors >= self.max_poll_retries:
                        raise RuntimeError("Poll errors exceeded limit")
                    time.sleep(_backoff(errors))
                    continue

                data = resp.json()
                errors = 0

                if data["request"] == "CAPCHA_NOT_READY":
                    continue
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(f"Solve error: {data['request']}")

            except (requests.ConnectionError, requests.Timeout):
                errors += 1
                if errors >= self.max_poll_retries:
                    raise

        raise TimeoutError("Poll timeout")

    def get_stats(self):
        return self.stats


# Usage
solver = RetrySolver("YOUR_API_KEY")

token = solver.solve(
    "userrecaptcha",
    googlekey="SITE_KEY",
    pageurl="https://example.com",
)

print(solver.get_stats())

Словарь self.stats — не декоративный: это первое, что стоит выгружать в мониторинг. Если счётчик retry растёт быстрее, чем total, а success топчется на месте, проблема почти наверняка не в самом CaptchaAI API, а в задаче — неверный sitekey, устаревший pageurl или заблокированный целевой сайт.


Circuit breaker: когда стоит остановиться

Повторы решают проблему единичного сбоя. Они не решают проблему, когда API стабильно недоступен несколько минут подряд — в этом случае долбить in.php каждую секунду означает жечь тайм-ауты и раздувать очередь без всякой пользы. Circuit breaker переводит клиент в состояние «открыто» после серии подряд идущих сбоев и на время вообще перестаёт отправлять запросы:

class CircuitBreaker:
    """Stop requests when the service appears down."""

    def __init__(self, failure_threshold=5, recovery_time=60):
        self.failure_threshold = failure_threshold
        self.recovery_time = recovery_time
        self.failures = 0
        self.last_failure = 0
        self.state = "closed"  # closed=normal, open=blocking

    def can_proceed(self):
        if self.state == "closed":
            return True
        # Check if recovery time has passed
        if time.time() - self.last_failure > self.recovery_time:
            self.state = "half-open"
            return True
        return False

    def record_success(self):
        self.failures = 0
        self.state = "closed"

    def record_failure(self):
        self.failures += 1
        self.last_failure = time.time()
        if self.failures >= self.failure_threshold:
            self.state = "open"
            print(f"Circuit OPEN — pausing for {self.recovery_time}s")


# Integrate with solver
breaker = CircuitBreaker(failure_threshold=5, recovery_time=60)


def solve_with_breaker(method, **params):
    if not breaker.can_proceed():
        raise RuntimeError("Circuit open — API appears unavailable")

    try:
        token = solver.solve(method, **params)
        breaker.record_success()
        return token
    except RuntimeError:
        breaker.record_failure()
        raise

Состояние half-open — это разовая проверка: как только recovery_time истёк, схема пропускает один запрос, чтобы понять, ожил ли сервис, вместо того чтобы сразу открыть шлюз для всей накопившейся очереди. Для большинства пайплайнов порог в 5 подряд идущих сбоев и минута ожидания — разумная отправная точка. Для высоконагруженных воркеров с сотнями параллельных потоков стоит поднять recovery_time до 2–3 минут, чтобы breaker не открывался и не закрывался слишком часто на обычных сетевых колебаниях.


Типичные ошибки при внедрении retry-логики

Большинство проблем с повторами — это не баги API, а баги в самой логике повторов. Проверьте себя по списку:

Проблема Причина Как исправить
Повторяются постоянные ошибки Ошибка не отфильтрована по типу Сверяйтесь со списком PERMANENT_ERRORS перед повтором
Бесконечный цикл повторов Нет верхнего предела попыток Всегда задавайте max_retries
Задержка между попытками слишком мала Фиксированный интервал вместо экспоненциального Используйте экспоненциальную задержку с джиттером
Все повторы дают один и тот же результат Проблема не временная Проверьте API-ключ, баланс и параметры запроса
Circuit breaker не срабатывает failure_threshold слишком высокий для реальной нагрузки Снизьте порог или считайте сбои в коротких окнах времени

Часто задаваемые вопросы

Сколько раз повторять отправку задачи, прежде чем сдаться?

3–5 попыток на отправку и 2–3 на опрос — этого достаточно почти всегда. Если пятая попытка отправки так и не прошла, проблема, скорее всего, не в перегрузке очереди, а в конфигурации: ключе, балансе или параметрах запроса.

Как отличить временную ошибку от постоянной?

По полю request в ответе API. ERROR_NO_SLOT_AVAILABLE, HTTP 429/500/502/503 и обрывы соединения — временные, их стоит повторять. ERROR_WRONG_USER_KEY, ERROR_KEY_DOES_NOT_EXIST и ERROR_ZERO_BALANCE — постоянные: сначала устраните причину, повтор её не решит.

Как часто опрашивать res.php, не создавая лишней нагрузки?

Раз в 5 секунд — стандартный и достаточный интервал, использованный в примере выше. Более частый опрос почти не ускоряет получение результата (решение всё равно занимает своё время), а более редкий увеличивает задержку до ответа без реальной экономии запросов.

Нужен ли circuit breaker при небольшом объёме запросов?

Да, хотя порог стоит держать ниже, чем в примере. Даже при десятке запросов в час пара минут простоя API без circuit breaker означает, что каждый из них уйдёт в полный цикл повторов и тайм-аутов — breaker останавливает это раньше и экономит время выполнения пайплайна.

Что делать, если ERROR_CAPTCHA_UNSOLVABLE повторяется на одной и той же задаче?

Не повторяйте запрос с тем же ID задачи — он не даст другого результата. Отправьте новый запрос с исходным изображением или обновлённым pageurl/sitekey, если источник CAPTCHA мог измениться.


Связанные материалы


Соберите retry-логику один раз — и пайплайн сам переживёт сетевые сбои. Попробуйте CaptchaAI и подключите её к своему воркеру.

Комментарии для этой статьи отключены.