Khắc Phục Sự Cố

Token Cloudflare Turnstile hết hạn sau bao lâu và cách tránh race condition khi submit

Token Cloudflare Turnstile chỉ sống khoảng 300 giây (5 phút) kể từ lúc được tạo ra — không phải từ lúc CaptchaAI trả token về cho bạn, càng không phải từ lúc code của bạn nhận được nó. Đây là nguyên nhân phổ biến nhất khiến một luồng automation chạy mượt trên máy test nhưng lại rớt liên tục ở production.

CaptchaAI thường trả token Turnstile về trong dưới 10 giây, nên phần lớn quỹ 300 giây đó nằm ở chỗ code của bạn xử lý gì sau khi nhận token. Bài này đi thẳng vào dấu hiệu nhận biết, các kịch bản hay gặp và ba chiến lược để không bao giờ gửi một token đã chết.

Dấu hiệu cho biết token đã hết hạn

Trang web thường không báo thẳng "token đã hết hạn". Hãy để ý các tín hiệu sau:

Tín hiệu Ý nghĩa
HTTP 403 sau khi gửi token Token không hợp lệ hoặc đã hết hạn
Bị chuyển hướng ngược về trang form Xác minh token thất bại
Thông báo lỗi "verification failed" Lỗi chung — nhiều khả năng do hết hạn
Trang thử thách (challenge) xuất hiện lại Token bị từ chối, Cloudflare yêu cầu thử thách lại

Nếu lỗi chỉ xảy ra sau khi luồng từng submit thành công, gần như chắc chắn là do hết hạn — không phải sitekey hay pageurl sai (lỗi đó thường lộ ra ngay từ lần thử đầu tiên).

Token Turnstile sống được bao lâu, và đồng hồ tính từ đâu

Loại CAPTCHA Thời gian sống của token
reCAPTCHA v2/v3 ~120 giây
Cloudflare Turnstile ~300 giây
hCaptcha ~120 giây

300 giây rộng rãi hơn ~120 giây của reCAPTCHA, nhưng race condition vẫn xảy ra trong quy trình nhiều bước — vì đồng hồ bắt đầu chạy khi Cloudflare tạo ra token, không phải khi CaptchaAI trả nó về cho bạn.

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

Đồng hồ tính từ mốc 0:20, không phải 0:25. Về lý thuyết bạn có đến khoảng 5:20 để submit — nhưng thực tế thường không rộng rãi như vậy.

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

Chỉ vài bước chuyển trang, một dialog phát sinh, hoặc một lần mạng chậm là quỹ 300 giây bay hết.

Các kịch bản dễ dính race condition nhất

Ba tình huống dưới đây chiếm phần lớn các ca lỗi token Turnstile trong thực tế:

  • Form nhiều bước — CAPTCHA nằm ở giữa luồng, submit thật sự diễn ra ở cuối.
  • Xử lý batch — giải trước một loạt token rồi mới dùng dần.
  • Retry bằng token cũ — dùng lại token sau khi submit thất bại.

Form nhiều bước

Step 1: Fill personal info → Step 2: Fill address → 
Step 3: Solve CAPTCHA → Step 4: Review → Step 5: Submit

Nếu CAPTCHA ở Bước 3 nhưng submit diễn ra ở Bước 5, độ trễ hoàn toàn có thể vượt 5 phút — nhất là khi có bước review thủ công. Tình huống này quen thuộc với team QA/automation tại Việt Nam đang test luồng đặt lịch hẹn nhiều bước (ví dụ hệ thống visa dạng BLS CAPTCHA) hoặc checkout nhiều trang trên sàn thương mại điện tử: nếu CAPTCHA được giải ngay ở bước đầu, token gần như chắc chắn hết hạn trước khi luồng chạy tới bước cuối.

Xử lý hàng loạt theo batch

# 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

Retry bằng token cũ

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

Dù token chưa hết hạn theo thời gian, trang web có thể đã đánh dấu nó "đã tiêu thụ" và từ chối lần thứ hai.

Ba cách ngăn token hết hạn

Chiến lược 1: Giải token đúng lúc (just-in-time)

Chỉ yêu cầu token khi bạn đã sẵn sàng submit ngay:

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

Chiến lược 2: Theo dõi tuổi token trong code

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)

Chiến lược 3: Token mới cho mỗi lần retry (JavaScript)

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');
}

Ghi log để xác nhận nghi ngờ

Cách rẻ nhất để xác nhận đúng là do hết hạn: log tuổi token (age) ngay tại thời điểm submit.

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)")

Nếu log cho thấy age thường xuyên vượt 270 giây đúng vào lúc lỗi xảy ra, đó chính là nguyên nhân.

Turnstile tự làm mới token — nhưng chỉ khi có trình duyệt

Trong luồng chạy qua trình duyệt thật, widget Turnstile tự làm mới token trước khi nó hết hạn. Callback data-expired-callback kích hoạt đúng lúc token hết hạn:

turnstile.render('#captcha', {
  sitekey: '0x4AAAA...',
  callback: (token) => {
    console.log('New token:', token);
  },
  'expired-callback': () => {
    console.log('Token expired — widget will auto-refresh');
  }
});

Trong automation chỉ gọi API (không có trình duyệt), bạn không được hưởng cơ chế tự làm mới này — quản lý tuổi token là trách nhiệm của code bạn, đúng lý do vì sao ba chiến lược ở trên tồn tại.

Bảng khắc phục nhanh

Vấn đề Nguyên nhân Cách xử lý
Token chạy được lúc test nhưng lỗi ở production Luồng production chậm hơn luồng test Giải token đúng lúc (just-in-time), không giải trước
Lần submit đầu thành công, các lần retry sau đều lỗi Dùng lại token đã bị tiêu thụ Giải một token mới cho mỗi lần thử
Lỗi rời rạc trên các form dài Token hết hạn giữa quy trình nhiều bước Chuyển bước giải CAPTCHA xuống ngay trước bước submit cuối
Job chạy batch có tỷ lệ lỗi cao Token giải hàng loạt hết hạn trước khi kịp dùng Giải token theo yêu cầu (on-demand), không giải theo lô

Câu hỏi thường gặp

Token Turnstile sống được bao lâu?

Khoảng 300 giây (5 phút) kể từ lúc Cloudflare tạo ra token, không phải từ lúc bạn nhận được nó. Dùng 270 giây làm mốc an toàn thực tế trong code.

Có cách nào kéo dài thời gian sống của token không?

Không. Thời hạn do Cloudflare quy định, không thể chỉnh sửa. Lựa chọn duy nhất khi token gần hết hạn là giải một token mới.

Vì sao form test qua được nhưng lên production lại lỗi liên tục?

Gần như luôn do độ trễ: network chậm hơn, nhiều bước xác thực hơn, tải trang thật — cộng lại dễ vượt 5 phút nếu bạn giải token quá sớm.

Có nên giải token trước để tiết kiệm thời gian không?

Chỉ nên nếu luồng chắc chắn dùng token trong vòng vài phút. Với job batch, hãy giải theo yêu cầu thay vì giải trước rồi lưu lại.

Bài viết liên quan

Bước tiếp theo

Lấy API key CaptchaAI và áp dụng nguyên tắc giải-token-đúng-lúc ở trên — CaptchaAI trả token Turnstile trong dưới 10 giây với tỷ lệ giải thành công cao trên các loại CAPTCHA được hỗ trợ.

Os comentários estão desativados para este artigo.