Cara Mengatasi HTTP 429 Too Many Requests Saat Scraping

http 429 too many requests - How to Fix HTTP 429 Too Many Requests While Scraping

HTTP 429 Too Many Requests berarti melambat, bukan pergi. Server sedang memberi tahu bahwa ia masih menginginkan trafik Anda, hanya dengan laju lebih rendah, dan biasanya ia menyebutkan persis berapa lama Anda harus menunggu. Karena itu solusinya hampir tidak pernah berupa proxy atau user agent baru. Solusinya adalah membaca satu header, tidur dengan benar, dan membatasi berapa banyak request yang sedang berjalan sekaligus. Artikel ini membahas ketiganya, lalu menunjukkan cara membedakan rate limit sungguhan dari blokir bot yang memakai kode status yang sama, sebab keduanya menuntut respons yang berlawanan.

Apa yang Anda butuhkan

  • Python 3.10 atau lebih baru, dengan library requests, untuk contoh-contohnya. Logikanya bisa langsung dipindahkan ke HTTP client mana pun.
  • Sebuah terminal, supaya Anda bisa melihat header respons sebelum menulis kode retry apa pun.
  • CapSkip berjalan, dan ini hanya untuk bagian terakhir, entah dalam mode Local di alamat loopback atau dalam mode Server di mesin yang bisa dijangkau worker Anda. Keduanya dibahas di pengaturan koneksi, jadi pilih satu sebelum mulai.

Langkah 1: baca responsnya sebelum Anda mengulanginya

Sebagian besar penanganan 429 ditulis tanpa melihat apa pun, dan itulah sebabnya ia tidak bekerja. Lihat dulu responsnya yang sebenarnya. Kode status datang bersama header yang memberi tahu berapa batasnya dan kapan ia direset, dan layanan yang berbeda memakai header yang berbeda.

# No install needed. Dump headers, throw the body away.
curl -sS -o /dev/null -D - "https://example.com/api/items?page=2"

# Look for these, in this order of usefulness:
#   Retry-After: 30           seconds, or an HTTP date
#   RateLimit-Reset: 1724500000
#   X-RateLimit-Remaining: 0
#   RateLimit-Limit: 100

Yang paling menentukan adalah Retry-After. Header ini didefinisikan tepat untuk situasi seperti ini dan datang dalam dua bentuk: jumlah detik untuk menunggu, atau tanggal HTTP absolut. Keduanya sah, keduanya muncul di lapangan, dan kode yang menganggap isinya selalu angka akan pecah di situs yang mengirim tanggal. MDN mendokumentasikan kedua bentuknya, dan halaman referensinya tentang kode status 429 juga layak diberi dua menit waktu Anda.

Parse dengan defensif, patuhi kalau ada, dan kembali ke jadwal Anda sendiri kalau tidak ada:

# pip install requests
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

def retry_delay(response, fallback):
    """Seconds to wait, from Retry-After if the server sent one."""
    raw = response.headers.get("Retry-After")
    if not raw:
        return fallback
    try:
        return max(0.0, float(raw))          # the delay-seconds form
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)    # the HTTP-date form
        return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError):
        return fallback

Batasi apa pun yang kembali. Server yang meminta 3600 detik sedang menyuruh Anda berhenti selama satu jam, dan worker yang dengan patuh tidur selama itu di dalam sebuah request handler akan tampak seperti hang bagi semua yang ada di atasnya. Ambil yang lebih kecil antara nilai header dan batas atas Anda sendiri, lalu putuskan terpisah apakah pekerjaannya perlu ditunda.

Langkah 2: mundur secara eksponensial, dengan jitter

Kalau tidak ada Retry-After yang bisa diikuti, gandakan waktu tunggu setiap kali dan tambahkan keacakan. Penggandaan itulah yang mencegah Anda terus menghajar layanan yang sudah kepayahan. Keacakannya mencegah dua puluh worker Anda sendiri, yang semuanya menabrak batas di detik yang sama, mengulang request di detik yang sama untuk selamanya.

# pip install requests
import random, time, requests

def get_with_backoff(url, attempts=5, base=1.0, ceiling=60.0):
    for attempt in range(attempts):
        response = requests.get(url, timeout=30)
        if response.status_code != 429:
            return response
        # Full jitter: sleep somewhere in [0, base * 2 ** attempt].
        window = min(ceiling, base * (2 ** attempt))
        delay = retry_delay(response, random.uniform(0, window))
        time.sleep(min(delay, ceiling))
    raise RuntimeError(f"still rate limited after {attempts} attempts")

# Waits land near 0-1s, 0-2s, 0-4s, 0-8s, 0-16s unless the
# server named a delay, in which case that wins.

Full jitter, yaitu nilai acak antara nol dan jendelanya alih-alih jendela ditambah goyangan kecil, adalah varian yang paling cepat membuat sebuah armada tidak lagi serempak. Kalau Anda tidak ingin menulisnya sendiri, urllib3 sudah menyediakannya, tetapi ada dua argumen yang harus disetel eksplisit agar berguna:

# pip install requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(
    total=5,
    # status_forcelist defaults to none, so 429 is NOT retried
    # unless you list it here yourself. This is the usual bug.
    status_forcelist=[429, 500, 502, 503, 504],
    backoff_factor=1,        # 1 * 2 ** previous_retries seconds
    backoff_jitter=1.0,      # urllib3 2.x only
    allowed_methods=["GET", "HEAD"],
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

Ada dua detail yang perlu diketahui tentang class itu. Retry-After sudah dipatuhinya sendiri, karena 429 adalah salah satu dari tiga kode status di set RETRY_AFTER_STATUS_CODES miliknya bersama 413 dan 503, dan respect_retry_after_header bernilai true secara default. Tetapi status_forcelist secara default kosong sama sekali, jadi objek Retry yang baru tidak mengulang 429 sampai Anda menyebutkannya di sana. Orang biasanya mengira sebaliknya, lalu bingung kenapa adapter-nya tidak melakukan apa pun.

Langkah 3: batasi concurrency alih-alih retry lebih keras

Logika retry hanya mengobati gejala. Kalau 429 datang stabil dan bukan meledak-ledak, Anda memang meminta lebih dari yang diizinkan, dan solusinya adalah mengirim lebih sedikit. Sebuah semaphore ditambah jarak minimum antar-request memperbaiki lebih banyak kasus rate limiting daripada kurva backoff apa pun.

# Standard library only.
import asyncio

# Six in flight is a sane starting point for an unknown API.
gate = asyncio.Semaphore(6)
MIN_GAP = 0.2          # seconds between starts, per worker

async def fetch(client, url):
    async with gate:
        response = await client.get(url)
        await asyncio.sleep(MIN_GAP)
        return response

# Tune down on the first 429, and stay there for a while.
# Tuning back up too eagerly just rediscovers the limit.

Dorongan pertama setelah 429 adalah menyebar beban yang sama ke lebih banyak IP. Untuk sebagian target itu berhasil, tetapi itu keputusan terpisah dengan konsekuensinya sendiri, yang kami bahas terpisah di panduan rotasi proxy CAPTCHA. Lakukan sebagai keputusan kapasitas, bukan sebagai cara menghindari membaca sebuah header.

429 bukan 403, dan challenge bukan keduanya

Di sinilah penanganan 429 salah dengan biaya paling mahal. Rate limiting dan deteksi bot adalah sistem berbeda yang kadang berbagi satu kode status, dan keduanya menginginkan hal yang berlawanan dari Anda. HTTP 429 Too Many Requests dari sebuah rate limiter adalah instruksi penjadwalan, sedangkan kode yang sama dari edge anti-bot adalah penolakan. Mundur dengan sopan di depan blokir bot membuang satu jam. Mengulang dengan keras pada rate limit sungguhan membuat IP Anda diblokir.

Apa yang Anda dapatBiasanya artinya apaApa yang sebenarnya menolong
429 dengan header Retry-AfterRate limit yang nyata dan terdokumentasiTunggu tepat selama itu, lalu turunkan laju Anda
429 tanpa header dan dengan body HTMLLapisan edge atau anti-bot, bukan API-nyaPerlakukan sebagai blokir, bukan sebagai batas
403 yang datang seketikaFingerprint, TLS, atau reputasi IPPerbaiki client-nya, karena menunggu tidak mengubah apa pun
503 dengan Retry-AfterKelebihan beban atau sedang pemeliharaanJalur backoff yang sama seperti 429
200 yang membawa halaman challengeAnda sudah dinilai lalu diselaPecahkan challenge-nya dan lanjutkan

Baris terakhir itu sering mengecoh, karena tidak ada yang gagal. Request-nya mengembalikan 200 dan body-nya berisi halaman challenge, bukan data Anda, jadi loop retry yang hanya melihat kode status akan dengan senang hati menghajarnya selamanya. Periksa penanda yang Anda harapkan di dalam body, bukan cuma baris statusnya. Kalau yang Anda temukan ternyata challenge, mundur bukan jawabannya, memecahkannya baru jawabannya.

Jaga loop solve Anda dari lubang yang sama

Loop polling yang menunggu jawaban CAPTCHA sendirinya adalah loop retry, dan versi buatan tangan melakukan persis kesalahan-kesalahan di atas. Ada dua hal pada CapSkip yang membuat ini lebih mudah dibandingkan layanan bermeter. Ia berjalan di hardware Anda sendiri, jadi tidak ada kuota per pemecahan yang bisa habis dan tidak ada rate limit miliknya sendiri yang bisa ditabrak. Dan SDK-nya sudah mundur otomatis untuk Anda: polling dimulai dari seperempat detik lalu tumbuh sampai pollingInterval, yang merupakan batas atas dan bukan jarak tetap.

# pip install capskip
from capskip import CapSkip, NetworkException, TimeoutException

# Local mode. In Server mode, host is the solver box's address.
solver = CapSkip(host="127.0.0.1", port=8080, pollingInterval=2)

try:
    result = solver.recaptcha(
        sitekey="YOUR_SITEKEY",
        url="https://example.com/page-with-recaptcha",
    )
    print(result["code"][:24])   # token, submit it with the form
except TimeoutException:
    # Polling ran past recaptchaTimeout, 300 seconds by default.
    print("gave up waiting, try again or lower the timeout")
except NetworkException:
    # The solver is not reachable on that host and port.
    print("check CapSkip is running and the mode you set")

Menurunkan pollingInterval membuat jawaban datang lebih cepat dan tidak berbiaya apa pun, dan pilihan itu praktis tidak Anda miliki di API yang ditagih. Kalau Anda melakukan polling ke endpoint HTTP mentah alih-alih memakai SDK, jeda yang disarankan untuk setiap tipe CAPTCHA tercantum di dokumentasi API, beserta respons tertunda yang akan Anda terima selagi sebuah pemecahan masih berjalan.

Menjalankan pemecah di server saja

Rate limiting biasanya masalah sebuah armada, bukan satu skrip, dan armada tidak berbagi satu alamat loopback. Pengaturan koneksi mencakup kedua kasus:

ModeMendengarkan diGunakan saat
Lokal127.0.0.1, hanya perangkat ituScraper Anda dan pemecah berjalan di satu mesin
ServerAlamat jaringan atau IP publik AndaWorker, container, sebuah VPS, atau platform terkelola memanggil lewat API

Arahkan host SDK ke mesin tempat pemecah berada, dan tidak ada lagi yang berubah di kode Anda, sehingga sepuluh worker bisa berbagi satu instance. IP publik statis disarankan kalau pemanggilnya berada di luar jaringan Anda sendiri. Detailnya ada di pengaturan koneksi, dan mode Server tetap hardware Anda dan tetap tanpa meter: yang berpindah adalah tempat pemecah berjalan, bukan siapa pemiliknya.

FAQ

Haruskah saya selalu menuruti Retry-After?

Patuhi, tetapi beri batas atas. Itu sinyal paling bisa dipercaya soal kapan jendelanya terbuka lagi, jadi mengabaikannya berarti menebak lebih buruk daripada apa yang sudah dikatakan server. Nilai satu jam adalah keputusan yang berbeda: parkir pekerjaannya dan kembali nanti, jangan menahan worker tetap tertidur, karena semua yang di atasnya akan membaca itu sebagai hang.

Bagaimana cara membedakan rate limit dari blokir bot?

Lihat apa yang datang bersama kode statusnya. Batas yang nyata bisa dibaca mesin: sebuah header Retry-After atau RateLimit, body JSON yang kecil, dan perilaku yang konsisten ketika Anda menunggu. Blokir bot mengirim halaman HTML, tanpa informasi waktu, dan sering memberi respons yang sama tak peduli berapa lama Anda membiarkannya. Yang kedua ingin client yang berbeda, bukan tidur yang lebih panjang.

Apakah pemecah pernah mengembalikan 429?

Tidak. CapSkip berjalan di hardware yang Anda kendalikan tanpa kuota per pemecahan, jadi tidak ada jendela penagihan yang bisa habis dan tidak ada batas di sisi hulu yang bisa ditabrak. Kalau sebuah panggilan solve gagal, Anda akan mendapat error jaringan karena daemon-nya tidak terjangkau, atau timeout karena polling melewati batas atasnya. Keduanya menandakan sesuatu yang lokal, jadi periksa host, port, dan mode yang Anda konfigurasikan.

Worker saya berjalan di platform terkelola. Pemecahnya ditaruh di mana?

Di mesin milik Anda yang bisa dijangkau platform itu, dengan pemecah dialihkan ke mode Server. Runner terkelola dan platform automation terkelola tidak bisa melihat alamat loopback Anda, jadi ikat API ke alamat jaringan atau IP publik Anda lalu arahkan setiap worker ke sana. Satu instance melayani seluruh armada dan tidak perlu tunnel.

Versi paling singkatnya

HTTP 429 Too Many Requests adalah masalah penjadwalan, jadi perlakukan begitu. Baca Retry-After dan patuhi sampai batas atas yang Anda tentukan sendiri. Kembali ke backoff eksponensial dengan full jitter kalau header-nya tidak ada. Lalu turunkan concurrency Anda, karena 429 yang datang stabil adalah masalah kapasitas yang tidak diselesaikan kurva retry mana pun. Dan periksa body-nya sebelum Anda mengulang sama sekali, sebab halaman challenge datang dengan kode status yang sangat sehat dan menuntut dipecahkan, bukan ditunggu. Untuk bagian itu, pemecah captcha tanpa batas yang berjalan lokal adalah komponen yang pas, dan bagaimana ia masuk ke sebuah worker pool dijelaskan catatan tentang Pemecah CAPTCHA untuk web scraping secara rinci.