Токен Cloudflare Turnstile живёт около 300 секунд — и именно в это окно чаще всего укладывается разница между «работает на тестовом стенде» и «падает в production». Форма из пяти шагов, нестабильное мобильное соединение или токены, решённые заранее «про запас», — и к моменту отправки токен, который вы честно получили вовремя, уже недействителен. Разбираемся, откуда берётся этот тайминг и как выстроить рабочий процесс, чтобы условие гонки не всплывало на проде.
Сколько живёт токен
Токен Cloudflare Turnstile действителен примерно 300 секунд (5 минут) с момента создания. Это заметно больше, чем ~120 секунд у reCAPTCHA, но в многошаговых сценариях даже такого запаса не всегда хватает.
| Тип CAPTCHA | Срок действия токена |
|---|---|
| reCAPTCHA v2/v3 | ~120 секунд |
| Cloudflare Turnstile | ~300 секунд |
| hCaptcha | ~120 секунд |
Важная деталь. Таймер запускается в момент, когда Cloudflare сгенерировал токен, — не когда CaptchaAI вернул его вам, и не когда ваш код его фактически получил. Эти несколько секунд между генерацией и получением тоже списываются со счётчика.
Условие гонки по секундам
Time 0:00 — You submit a Turnstile task to CaptchaAI
Time 0:15 — CaptchaAI begins solving
Time 0:20 — Token is generated (timer starts here)
Time 0:25 — CaptchaAI returns token to you
Time 0:25+ — Your code processes the token
Time ??? — Your code submits the token to the site
Что это значит на практике:
- отсчёт пошёл с отметки 0:20;
- формально дедлайн — примерно 5:20, то есть почти пять минут в запасе;
- звучит как большой запас — но посмотрите, что происходит в реальном рабочем процессе, а не в синтетическом тесте:
Time 0:20 — Token generated
Time 0:25 — Received by your code
Time 0:30 — Fill form fields
Time 0:35 — Navigate to next page
Time 1:00 — Handle additional dialogs
Time 2:00 — Wait for page load
Time 4:00 — Network latency spike
Time 5:30 — Submit token → EXPIRED
Ни один из шагов сам по себе не выглядит медленным. Но они складываются, и всплеск задержки сети на пятой минуте — обычное дело, а не аномалия.
Где обычно теряют токен
1. Многошаговые формы
Формы, где решение CAPTCHA стоит не на последнем шаге, а где-то в середине цепочки:
Step 1: Fill personal info → Step 2: Fill address →
Step 3: Solve CAPTCHA → Step 4: Review → Step 5: Submit
Если CAPTCHA решается на шаге 3, а отправка — только на шаге 5, задержка между решением и submit легко превышает 5-минутный лимит, особенно если пользователь (или ваш скрипт) задерживается на промежуточных экранах.
2. Пакетное решение «про запас»
Частая ошибка при массовой обработке: решить все токены заранее, а использовать их позже.
# DON'T: Solve all tokens first, then use them
tokens = []
for url in urls:
tokens.append(solve_turnstile(url)) # Tokens age while waiting
for url, token in zip(urls, tokens):
submit_form(url, token) # Early tokens may be expired
Первые токены в очереди «стареют», пока решаются остальные, и к моменту использования часть из них уже истекла.
3. Повтор попытки со старым токеном
Ещё один типичный баг — переиспользование одного и того же токена после неудачной отправки:
token = solve_turnstile(site_key, page_url)
for attempt in range(3):
result = submit_form(page_url, token)
if result.ok:
break
# BUG: Retrying with the same token — it may be expired OR already consumed
Токен, который сайт уже один раз проверил (даже неуспешно), может считаться использованным вне зависимости от срока действия — повтор той же строки почти всегда провалится.
Пример из практики. Команда, которая тестирует checkout-форму для клиента с пользователями из Казахстана и Беларуси, столкнулась именно со сценарием №1: на мобильных сетях с нестабильным соединением загрузка промежуточных шагов иногда занимала больше минуты, и токен, решённый на третьем экране, «протухал» ещё до подтверждения заказа. Перенос решения CAPTCHA на последний шаг снял проблему полностью — без единой строчки правок в самой форме. Если в рамках такого тестирования вы логируете запросы пользователей, собирайте только те данные, которые вправе обрабатывать по договору с клиентом (для аудитории РФ это прямо требует 152-ФЗ, для трансграничных проектов — эквивалентная дисциплина в духе GDPR).
Как решать токен «точно в срок»
Стратегия 1: решение непосредственно перед отправкой
Запрашивайте токен только тогда, когда форма реально готова к submit — не раньше:
import requests
import time
def solve_turnstile(site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
# Complete all form steps FIRST
fill_personal_info()
fill_address()
navigate_to_review()
# THEN solve and submit immediately
token = solve_turnstile(site_key, page_url)
submit_form(token) # Submit within seconds of receiving the token
Стратегия 2: отслеживание возраста токена
Если между решением и отправкой всё же есть разрыв, оборачивайте токен в объект, который сам знает, годен он ещё или нет:
import time
class TimedToken:
def __init__(self, token, created_at=None):
self.token = token
self.created_at = created_at or time.time()
self.max_age = 270 # 4.5 min — safety margin from 5 min limit
@property
def is_valid(self):
return (time.time() - self.created_at) < self.max_age
@property
def remaining_seconds(self):
return max(0, self.max_age - (time.time() - self.created_at))
# Usage
timed_token = TimedToken(solve_turnstile(site_key, page_url))
# Check before using
if timed_token.is_valid:
submit_form(timed_token.token)
else:
# Solve a fresh token
timed_token = TimedToken(solve_turnstile(site_key, page_url))
submit_form(timed_token.token)
Обратите внимание на запас: max_age = 270 секунд, а не полные 300 — это сознательный буфер на сетевые задержки и погрешность самого таймера Cloudflare.
Стратегия 3: свежий токен на каждой попытке (JavaScript)
Для retry-логики правило простое: ни одна повторная попытка не должна отправлять токен, полученный для предыдущей:
async function submitWithFreshToken(siteKey, pageUrl, formData) {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Always solve a fresh token for each attempt
const token = await solveTurnstile(siteKey, pageUrl);
const response = await fetch(pageUrl, {
method: 'POST',
body: JSON.stringify({ ...formData, 'cf-turnstile-response': token }),
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) return await response.json();
console.log(`Attempt ${attempt + 1} failed, solving fresh token...`);
}
throw new Error('All attempts failed');
}
Как понять, что дело именно в истёкшем токене
Сайт почти никогда прямо не пишет «токен истёк». Об этом обычно сигнализируют косвенные признаки:
| Сигнал | О чём говорит |
|---|---|
| HTTP 403 после отправки токена | Токен недействителен или его срок истёк |
| Редирект обратно на страницу формы | Проверка токена на сервере не прошла |
| Сообщение вида «Проверка не удалась» | Общая ошибка — истечение срока один из вероятных виновников |
| Испытание Cloudflare появляется повторно | Токен отклонён, Cloudflare переигрывает проверку заново |
Логирование для диагностики
Прежде чем гадать, добавьте в код возраст токена на момент отправки — это снимает большинство вопросов за один прогон:
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("turnstile")
token_received_at = time.time()
token = solve_turnstile(site_key, page_url)
logger.info(f"Token received, length: {len(token)}")
# ... workflow steps ...
submit_time = time.time()
age = submit_time - token_received_at
logger.info(f"Submitting token, age: {age:.1f}s")
if age > 270:
logger.warning(f"Token may be expired (age: {age:.1f}s > 270s safety limit)")
Автообновление в браузерных сценариях
В браузерных потоках виджет Turnstile сам обновляет токен до истечения срока действия. Колбэк data-expired-callback срабатывает именно в момент истечения:
turnstile.render('#captcha', {
sitekey: '0x4AAAA...',
callback: (token) => {
console.log('New token:', token);
},
'expired-callback': () => {
console.log('Token expired — widget will auto-refresh');
}
});
При автоматизации только через API, без реального браузера, этот механизм не работает — виджета попросту нет. Отслеживать свежесть токена в таком случае приходится вручную, стратегиями выше.
Частые ошибки и быстрые исправления
| Проблема | Причина | Исправление |
|---|---|---|
| Токен работает в тестах, но не в production | Реальный рабочий процесс медленнее тестового сценария | Решайте токен непосредственно перед отправкой, а не заранее |
| Первая отправка проходит, повторные — нет | Повторное использование уже проверенного (consumed) токена | Запрашивайте новый токен на каждую попытку |
| Периодические сбои на длинных формах | Токен истекает в процессе многошагового флоу | Перенесите решение CAPTCHA на последний шаг перед submit |
| Высокий процент ошибок в пакетных заданиях | Токены решены пачкой заранее и успевают устареть | Решайте токены по требованию, а не батчем |
FAQ
Как отличить истёкший токен от обычной ошибки проверки?
По прямому признаку это сделать сложно — Cloudflare редко возвращает явный код «expired». Логируйте возраст токена (см. раздел про логирование): если ошибка стабильно случается при возрасте токена ближе к 270–300 секундам, а не сразу после решения, причина почти наверняка в истечении срока.
Можно ли продлить срок действия токена Cloudflare Turnstile?
Нет. Время жизни токена задаёт Cloudflare, и оно не настраивается на стороне клиента или CaptchaAI. Единственный рабочий вариант — решить новый токен ближе к моменту отправки.
Что делать, если CAPTCHA решается на раннем шаге многошаговой формы?
Два рабочих варианта:
- по возможности перенесите виджет или запрос токена на последний шаг, непосредственно перед submit;
- если это невозможно из-за архитектуры формы, оборачивайте токен в объект с проверкой возраста (Стратегия 2) и перерешивайте его, если он «просрочен» к моменту отправки.
Почему пакетное решение токенов даёт больше ошибок, чем решение по одному?
Потому что первые токены в пакете ждут своей очереди, пока решаются остальные, и часть из них истекает ещё до использования. Решение «по требованию» — сразу перед конкретной отправкой — убирает эту проблему полностью.
Какой запас времени стоит закладывать на практике?
Используйте не полные ~300 секунд, а safety-лимит около 270 секунд (4,5 минуты) — этот запас закладывают в примерах кода выше. Он покрывает погрешность самого таймера Cloudflare и типичные сетевые задержки.