Задача CAPTCHA, не решившаяся после всех повторов, не обязана бесследно пропадать. Dead Letter Queue (DLQ, «очередь недоставленных сообщений») откладывает такие задачи в отдельное хранилище — для повтора, разбора причин или алерта. Без DLQ единственный след сбоя — строка в логе, которую никто не перечитывает.
Причины, по которым задача CAPTCHA попадает в DLQ
Обычно задача попадает в DLQ по одной из четырёх причин.
ERROR_CAPTCHA_UNSOLVABLE: решатель не справился с задачей
Решатель принял задачу, но не смог вернуть валидный токен — битый sitekey, нестандартная разметка страницы или защита сработала раньше, чем токен успел вернуться. Лишний повтор такую задачу обычно не спасает: разбирайтесь в причине, а не гоняйте её по кругу.
ERROR_NO_SLOT_AVAILABLE: не осталось свободных воркеров
Все потоки заняты, а лимит повторов уже исчерпан. Это чаще симптом пиковой нагрузки на вашей стороне, чем проблема самой задачи — если такие сбои идут пачками, сначала проверьте план по потокам, а не логику повтора.
Тайм-аут: решатель не уложился в отведённый срок
Опрос res.php завершился без результата за все отведённые попытки. Единичные тайм-ауты — норма для отдельных сложных задач; регулярные — повод посмотреть на нагрузку у решателя в конкретные часы.
Сетевая ошибка: соединение оборвалось во время опроса
Запрос к res.php не долетел или ответ не пришёл целиком. На нестабильном канале это происходит заметно чаще, чем на стабильном офисном интернете.
Типичный кейс — парсинг с защитой CAPTCHA из региона с нестабильным мобильным интернетом (например, удалённая команда в Казахстане): часть запросов к res.php обрывается до получения токена, и без ретраев это выглядит как случайные, необъяснимые провалы. Без DLQ такие сбои вылавливают вручную по логам, задним числом и почти всегда с опозданием.
Python: DLQ в памяти и логика повтора
Минимальная реализация: очередь на deque, копящая неудачные задачи в памяти на время работы скрипта.
import time
import json
import requests
from collections import deque
from dataclasses import dataclass, asdict
from typing import Optional
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class FailedTask:
sitekey: str
page_url: str
error: str
attempts: int
timestamp: float
task_id: Optional[str] = None
class DeadLetterQueue:
def __init__(self, max_size=1000, max_retries=3):
self._queue = deque(maxlen=max_size)
self.max_retries = max_retries
def push(self, task: FailedTask):
self._queue.append(task)
print(f"[dlq] Added: {task.error} (attempts: {task.attempts})")
def pop(self) -> Optional[FailedTask]:
return self._queue.popleft() if self._queue else None
def size(self) -> int:
return len(self._queue)
def peek_all(self) -> list:
return [asdict(t) for t in self._queue]
def export_json(self, path: str):
with open(path, "w") as f:
json.dump(self.peek_all(), f, indent=2)
print(f"[dlq] Exported {self.size()} tasks to {path}")
dlq = DeadLetterQueue(max_retries=3)
def solve_captcha(sitekey, page_url, max_retries=3):
for attempt in range(max_retries + 1):
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:
raise Exception(data["request"])
task_id = data["request"]
for _ in range(24):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
}, timeout=15).json()
if poll["status"] == 1:
return poll["request"]
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(poll["request"])
raise TimeoutError(f"Task {task_id} timed out")
except Exception as e:
if attempt == max_retries:
dlq.push(FailedTask(
sitekey=sitekey,
page_url=page_url,
error=str(e),
attempts=attempt + 1,
timestamp=time.time(),
))
return None
time.sleep(2 ** attempt)
return None
# Process a batch
urls = [f"https://example.com/page/{i}" for i in range(5)]
for url in urls:
token = solve_captcha("6Le-SITEKEY", url)
if token:
print(f"Solved: {token[:40]}...")
print(f"\nDLQ size: {dlq.size()}")
Ожидаемый результат:
Solved: 03AGdBq26ZfPxL...
Solved: 03AGdBq27AbCdE...
[dlq] Added: ERROR_CAPTCHA_UNSOLVABLE (attempts: 4)
Solved: 03AGdBq28FgHiJ...
[dlq] Added: Task 71823460 timed out (attempts: 4)
DLQ size: 2
Как повторно обработать задачи из DLQ
Когда основной пакет обработан, запустите retry_dlq() отдельным проходом — он забирает задачи из очереди и пробует решить их ещё раз, с собственным лимитом попыток:
def retry_dlq(dlq: DeadLetterQueue, max_retries=2):
retried = 0
recovered = 0
while dlq.size() > 0:
task = dlq.pop()
if task.attempts >= dlq.max_retries + max_retries:
print(f"[dlq] Permanently failed: {task.sitekey} — {task.error}")
continue
retried += 1
token = solve_captcha(
task.sitekey, task.page_url, max_retries=max_retries
)
if token:
recovered += 1
print(f"[dlq-retry] Recovered: {token[:40]}...")
print(f"[dlq] Retried: {retried}, Recovered: {recovered}")
# Run DLQ retry after main batch
retry_dlq(dlq)
JavaScript: DLQ с сохранением в файл
Очередь в памяти не переживает рестарт процесса — деплой, падение контейнера, перезапуск по расписанию. Версия на JavaScript пишет очередь на диск после каждого изменения:
const fs = require('fs');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const DLQ_FILE = './captcha-dlq.json';
class DeadLetterQueue {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
this.queue = this._load();
}
push(task) {
this.queue.push({
...task,
timestamp: Date.now(),
});
this._save();
console.log(`[dlq] Added: ${task.error} (attempts: ${task.attempts})`);
}
pop() {
const task = this.queue.shift();
if (task) this._save();
return task || null;
}
size() {
return this.queue.length;
}
_load() {
try {
return JSON.parse(fs.readFileSync(DLQ_FILE, 'utf8'));
} catch {
return [];
}
}
_save() {
fs.writeFileSync(DLQ_FILE, JSON.stringify(this.queue, null, 2));
}
}
const dlq = new DeadLetterQueue(3);
async function solveCaptcha(sitekey, pageurl, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: API_KEY, method: 'userrecaptcha', googlekey: sitekey, 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: API_KEY, 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(`Task ${taskId} timed out`);
} catch (err) {
if (attempt === maxRetries) {
dlq.push({ sitekey, pageurl, error: err.message, attempts: attempt + 1 });
return null;
}
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
}
// Process tasks
(async () => {
for (let i = 0; i < 5; i++) {
const token = await solveCaptcha('6Le-SITEKEY', `https://example.com/page/${i}`);
if (token) console.log(`Solved: ${token.substring(0, 40)}...`);
}
console.log(`DLQ size: ${dlq.size()}`);
})();
Типичные проблемы DLQ и их решения
| Проблема | Причина | Решение |
|---|---|---|
| DLQ растёт без остановки | Никто не разбирает очередь | Запускайте retry_dlq() по расписанию (cron) |
| Задача повторяется бесконечно | Нет проверки на максимум попыток | Сверяйте task.attempts перед повторной постановкой |
| Файл DLQ повреждён | Параллельная запись из процессов | Файловая блокировка или переход на Redis/БД |
| Задачи теряются при падении | DLQ живёт только в памяти | Файловая или Redis-based очередь вместо in-memory |
Анализ отказов в DLQ
Экспортируйте задачи и посмотрите на распределение ошибок — закономерности видны сразу:
# Export DLQ for analysis
dlq.export_json("failed-tasks.json")
# Analyze error distribution
from collections import Counter
errors = Counter(t["error"] for t in dlq.peek_all())
for error, count in errors.most_common():
print(f" {error}: {count}")
Что искать в этих данных:
- Один и тот же sitekey падает раз за разом — проверьте, не устарел ли параметр и не поменялась ли вёрстка страницы.
- Тайм-ауты кучкуются в одни и те же часы — сопоставьте всплеск с нагрузкой на API или с вашим собственным пиковым трафиком.
- Сетевые ошибки идут пачками — сначала проверьте прокси и канал связи, и только потом подозревайте решатель.
Держите в FailedTask только то, что нужно: sitekey, page_url, код ошибки, число попыток. Персональные данные из query-параметров обрежьте перед записью — гигиена логирования что по 152-ФЗ, что по GDPR.
Часто задаваемые вопросы
DLQ в памяти или постоянный — что выбрать?
Для короткоживущих скриптов хватает памяти: очередь исчезнет вместе с процессом, и это нормально. Для сервисов, которые перезапускаются, нужен файл или Redis — иначе накопленные задачи теряются при каждом рестарте.
Сколько потоков закладывать на повтор задач из DLQ?
Ориентируйтесь на долю задач, оседающих в DLQ, а не на общий трафик: если из 1000 падает 20–30, хватит нескольких потоков из плана STANDARD ($30/мес, 15 потоков). Очередь, растущая быстрее, чем разбирается, — сигнал поднять план, а не просто добавлять потоки.
Что делать, если DLQ переполняется быстрее, чем разбирается?
Сначала посмотрите на errors.most_common() — обычно за ростом очереди стоит одна причина: устаревший sitekey, нестабильный прокси или тайм-ауты в конкретные часы. Наращивать частоту retry_dlq() имеет смысл уже после этого.
Можно ли хранить в DLQ данные пользователей вместе с задачей?
Не стоит. Хватает sitekey, page_url, кода ошибки и числа попыток — этого достаточно и для повтора, и для разбора причин. Персональные данные в очереди — лишний риск без практической пользы.
Как DLQ сочетается с паттерном автоматического выключателя?
Да, это разные уровни защиты. Выключатель останавливает поток запросов при массовой недоступности решателя, а DLQ ловит отдельные задачи, упавшие до его срабатывания. См. схему автоматического выключателя для CAPTCHA API.
Не теряйте ни одной задачи CAPTCHA — постройте DLQ с CaptchaAI
Получите API-ключ на captchaai.com и подключите очередь к своему пайплайну.