Troubleshooting

Expiry Token reCAPTCHA: Timing Window dan Race Condition

Token reCAPTCHA tunduk pada dua batas keras:

  • 120 detik sejak token dibuat — bukan sejak kode Anda menerimanya.
  • Sekali pakai — token yang sudah dikirim tidak bisa dikirim ulang.

Langgar salah satunya dan Google mengembalikan timeout-or-duplicate. Jarak solve ke submit adalah satu-satunya variabel yang Anda kendalikan.

Aturan timing yang perlu Anda pegang

Aspek Rekomendasi
Jarak solve → submit Di bawah 90 detik (margin 30 detik)
Interval polling 5 detik per pemeriksaan res.php
Solve di awal Hanya jika submit pasti dalam 60 detik
Setelah timeout-or-duplicate Minta token baru
Antrean token Jangan pernah — tiap token expired sendiri
Operasi paralel Solve per task, bukan solve semua lalu pakai
Monitoring Catat usia token tiap submit, bukan hanya saat gagal

Kapan timer 120 detik mulai berjalan

Kesalahpahaman paling mahal: mengira timer mulai saat token sampai di variabel Anda. Timer mulai saat token dibuat, beberapa detik lebih awal.

Token generated (solve complete)
    ├─── 0s:   Token is valid ✓
    ├─── 60s:  Token is valid ✓
    ├─── 110s: Token is valid ✓  (but cutting it close)
    ├─── 119s: Token is valid ✓  (dangerous territory)
    └─── 120s: Token EXPIRED ✗  (timeout-or-duplicate)
Skenario Timer mulai saat...
Checkbox reCAPTCHA v2 Pengguna mencentang kotak
Grid gambar reCAPTCHA v2 Putaran pemilihan gambar terakhir selesai
reCAPTCHA v2 invisible Callback execute() mengembalikan token
reCAPTCHA v3 Promise execute() resolve dengan token
Solver API (CaptchaAI) Solver menghasilkan token (BUKAN saat Anda menerimanya)

Dengan jeda polling 5 detik, Anda praktis punya 115 detik. Masih longgar — sampai salah satu kondisi berikut muncul:

  • Ada pemrosesan tambahan antara menerima dan mengirim token
  • Beberapa formulir harus diisi sebelum submit
  • Server target lambat pada jam trafik puncak
  • Token disimpan di antrean dan dipakai belakangan
Solver generates token (timer starts)
    ↓ ~1-5 seconds (network + polling interval)
Your code receives token via res.php poll
    ↓ You now have ~115-119 seconds remaining
Your code processes and submits token
    ↓ ~1-10 seconds (depends on your workflow)
Target website validates token with Google
    ↓ ~1-2 seconds (Google API response time)
Total remaining after validation: ~105-117 seconds (comfortable)

Faktor jaringan ikut bermain. Worker di AWS ap-southeast-3 (Jakarta) atau ap-southeast-1 (Singapura) punya latency stabil, tetapi koneksi mobile atau VPS di region jauh menambah 2–4 detik per round trip. Ukur, jangan tebak: catat selisih respons res.php dan submit selama satu hari kerja.

Tiga race condition yang paling sering muncul

Race condition 1: solve paralel, submit berurutan

Terlihat efisien karena semua task dikirim sekaligus, padahal usia token pertama dan terakhir jauh berbeda saat dipakai.

# WRONG: Solving multiple CAPTCHAs in parallel, then submitting sequentially
tokens = []
for url in urls:
    task_id = solve_captcha(url)  # All submitted at t=0
    tokens.append(task_id)

# All tokens arrive around t=30
solved_tokens = [poll_result(tid) for tid in tokens]

# Sequential submission: first token at t=32, last at t=120+
for i, (url, token) in enumerate(zip(urls, solved_tokens)):
    submit_form(url, token)  # Later tokens may be expired!
    time.sleep(10)  # Each wait adds pressure

Perbaikan: solve dan kirim satu per satu, agar setiap token dipakai beberapa detik setelah dibuat.

# CORRECT: Solve and submit one at a time
for url in urls:
    token = solve_and_wait(url)  # Token received at t=30
    submit_form(url, token)      # Submitted at t=31 (89 seconds remaining)

Race condition 2: token diambil terlalu awal

Token yang diambil di awal alur terus menua selama pengisian formulir.

# WRONG: Pre-fetching tokens before knowing when they'll be used
token = solve_captcha()  # Token received at t=0

# ... user fills out form (30-120+ seconds) ...
# ... validation checks ...
# ... other processing ...

submit_form(token)  # Token may be expired!

Perbaikan: jadikan solve CAPTCHA langkah terakhir sebelum submit, bukan langkah pertama.

# CORRECT: Late-bind the CAPTCHA solve
prepare_form_data()   # Do everything that doesn't need the token
validate_inputs()     # Run validation before spending a token

# Now solve and submit immediately
token = solve_captcha()  # Token received at t=0
submit_form(token)       # Submitted at t=1 (119 seconds remaining)

Race condition 3: formulir multi-langkah

CAPTCHA muncul di langkah 1, submit final di langkah 3. Selama langkah 2 normal semuanya aman; begitu verifikasi tambahannya lambat, token kedaluwarsa tanpa peringatan.

# PROBLEMATIC: Multi-step form where CAPTCHA is on step 1 but submit is step 3
token = solve_captcha()       # t=0: Token received

fill_step_1(token)            # t=5: Step 1 submitted
response = fill_step_2()      # t=15: Step 2 completed
# ... step 2 has additional verification ...
wait_for_verification()       # t=60: Verification complete
fill_step_3_and_submit()      # t=65: Final submission (55 seconds remaining - OK)
# BUT if step 2 takes longer than expected...

Perbaikan: ukur usia token sebelum submit final, lalu solve ulang jika sudah melewati ambang aman.

token_received_at = time.time()
token = solve_captcha()
token_received_at = time.time()

# ... multi-step process ...

# Before final submission, check token age
token_age = time.time() - token_received_at
if token_age > 100:  # 20-second safety margin
    print(f"Token is {token_age:.0f}s old — requesting fresh token")
    token = solve_captcha()
    token_received_at = time.time()

submit_final(token)

Melacak usia token secara eksplisit

Ketiga perbaikan di atas punya satu inti: token butuh timestamp. Kelas berikut menyimpan waktu terima, menghitung sisa umur, dan solve ulang otomatis saat token basi.

import time
import requests

class TokenTimingManager:
    """Manage reCAPTCHA token timing to prevent expiration errors."""

    API_KEY = "YOUR_API_KEY"
    TOKEN_LIFETIME = 120
    SAFETY_MARGIN = 15  # seconds before expiry to consider "stale"

    def __init__(self, site_key, page_url, version="v2"):
        self.site_key = site_key
        self.page_url = page_url
        self.version = version
        self.current_token = None
        self.token_timestamp = None

    def _solve(self):
        """Request and poll for a new token."""
        params = {
            "key": self.API_KEY,
            "method": "userrecaptcha",
            "googlekey": self.site_key,
            "pageurl": self.page_url,
            "json": 1,
        }
        if self.version == "v3":
            params.update({"version": "v3", "action": "submit"})

        submit = requests.post("https://ocr.captchaai.com/in.php", data=params).json()
        task_id = submit["request"]

        for _ in range(60):
            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.get("status") == 1:
                self.current_token = result["request"]
                self.token_timestamp = time.time()
                return self.current_token

        raise TimeoutError("Token solve timeout")

    @property
    def token_age(self):
        """Seconds since current token was received."""
        if self.token_timestamp is None:
            return float("inf")
        return time.time() - self.token_timestamp

    @property
    def token_remaining(self):
        """Seconds remaining before token expires."""
        return max(0, self.TOKEN_LIFETIME - self.token_age)

    @property
    def is_fresh(self):
        """Whether the token is fresh enough to use."""
        return self.token_remaining > self.SAFETY_MARGIN

    def get_token(self):
        """Get a valid token, solving if current is stale or missing."""
        if self.current_token and self.is_fresh:
            return self.current_token

        return self._solve()

    def use_token(self):
        """Get and consume a token (cannot be reused)."""
        token = self.get_token()
        # Mark as consumed
        self.current_token = None
        self.token_timestamp = None
        return token

# Usage
manager = TokenTimingManager(
    site_key="6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    page_url="https://staging.example.com/qa-login",
)

# Get a fresh token right before submission
token = manager.use_token()
print(f"Token remaining: {manager.TOKEN_LIFETIME}s (fresh solve)")

# Submit form with token...

Margin 15 detik pada SAFETY_MARGIN adalah titik awal wajar. Naikkan ke 25–30 detik kalau validasi server target lambat; turunkan hanya setelah punya data latency submit yang konsisten.

Karena CaptchaAI menagih per thread, bukan per solve, solve ulang tidak menambah biaya per kejadian. Paket BASIC ($15/bulan, 5 thread) cukup untuk satu skrip QA; agensi price-monitoring dengan puluhan job paralel lebih pas di ADVANCE ($90/bulan, 50 thread). Polanya tetap: kirim task ke in.php, simpan task ID, polling res.php, pakai token.

Membaca kode error dengan benar

timeout-or-duplicate menutupi dua penyebab berbeda, dan usia token saat submit memisahkan keduanya.

def handle_recaptcha_error(error_codes, token_age_seconds):
    """Diagnose and handle reCAPTCHA validation errors."""

    if "timeout-or-duplicate" in error_codes:
        if token_age_seconds > 120:
            return {
                "cause": "Token expired (age: {:.0f}s > 120s)".format(token_age_seconds),
                "fix": "Reduce time between receiving and submitting token",
                "action": "re-solve",
            }
        elif token_age_seconds < 5:
            return {
                "cause": "Token likely reused (duplicate submission)",
                "fix": "Ensure each form submission gets a unique token",
                "action": "re-solve",
            }
        else:
            return {
                "cause": "Token may have been reused or server-side timing issue",
                "fix": "Check for double-submit in form handler",
                "action": "re-solve",
            }

    if "invalid-input-response" in error_codes:
        return {
            "cause": "Token is malformed or corrupted",
            "fix": "Check token transmission (URL encoding, field name)",
            "action": "re-solve",
        }

    return {"cause": "Unknown", "action": "investigate"}

Google sengaja tidak membedakan keduanya di pesan error, jadi logging usia token adalah satu-satunya cara membacanya:

  • Usia di atas 120 detik — murni masalah timing; perpendek jalur solve-ke-submit.
  • Usia jauh di bawah ambang tetapi tetap ditolak — curigai double-submit di handler formulir atau retry otomatis di HTTP client.
  • Pola yang sama berlaku pada Cloudflare Turnstile dan GeeTest v3, yang masa berlakunya juga terbatas.

Pertanyaan yang sering diajukan

Berapa lama token reCAPTCHA berlaku?

Tepat 120 detik sejak token dibuat. Batas ini diberlakukan server Google dan tidak bisa diperpanjang dari sisi klien maupun solver.

Token saya baru berumur 40 detik tapi tetap ditolak — kenapa?

Kemungkinan besar token itu sudah pernah dikirim. Handler double-submit, retry otomatis di HTTP client, atau tombol yang bisa diklik dua kali menghasilkan timeout-or-duplicate yang sama persis dengan token expired.

Apakah menambah thread membuat token bertahan lebih lama?

Tidak. Thread menentukan berapa banyak CAPTCHA diproses bersamaan, bukan masa berlaku token. Menambah thread membantu saat antrean solve menumpuk, bukan saat jarak solve-ke-submit bermasalah.

Bagaimana cara menguji margin timing tanpa menyentuh situs produksi?

Bangun halaman uji di staging.example.com/qa-login dengan sitekey Anda sendiri, lalu sisipkan jeda buatan antara solve dan submit. Naikkan jeda sampai error muncul — itulah margin aman nyata untuk wilayah deployment Anda.

Penutup

Dua batas keras, satu disiplin: solve sedekat mungkin dengan submit, simpan timestamp setiap token, solve ulang begitu usianya melewati ambang aman. Dengan CaptchaAI, solve ulang tidak menambah biaya karena penagihan berbasis thread.

Artikel Terkait

Komentar dinonaktifkan untuk artikel ini.