← Semua pembelajaran / Python untuk AI Engineer
Fase 3 · Async & HTTP

Tenacity

Retry dengan exponential backoff dan jitter — mekanisme wajib untuk memanggil API LLM yang punya rate limit.

Intisari

  • Satu dekorator @retry menggantikan puluhan baris loop retry manual.
  • Tiga bagian yang selalu kamu atur: stop (kapan menyerah), wait (jeda), retry (error mana yang layak diulang).
  • Wajib pakai exponential backoff dengan jitter: wait_random_exponential.
  • Jangan mengulang error 4xx selain 429 — request-nya memang salah, mengulang hanya membuang kuota.
  • Bekerja untuk fungsi sinkron maupun async def tanpa perubahan.

Masalahnya

Panggilan jaringan gagal secara sporadis: rate limit, server kelebihan beban, koneksi putus. Retry manual cepat berubah jadi kode yang berantakan dan rentan salah:

for percobaan in range(5):
    try:
        return panggil_api()
    except Exception:
        if percobaan == 4:
            raise
        time.sleep(2 ** percobaan)   # tanpa jitter — semua klien mengulang serentak
uv add tenacity
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(5), wait=wait_exponential(min=1, max=30))
def panggil_api():
    ...

Tiga bagian yang perlu diatur

1. stop — kapan menyerah

from tenacity import stop_after_attempt, stop_after_delay

stop=stop_after_attempt(5)                          # maksimal 5 percobaan
stop=stop_after_delay(60)                           # maksimal 60 detik total
stop=stop_after_attempt(5) | stop_after_delay(60)   # mana yang lebih dulu tercapai

2. wait — berapa lama jeda

from tenacity import wait_fixed, wait_exponential, wait_random_exponential

wait=wait_fixed(2)                                        # selalu 2 detik
wait=wait_exponential(multiplier=1, min=1, max=60)        # 1, 2, 4, 8, 16, 32, 60...
wait=wait_random_exponential(multiplier=1, max=60)        # eksponensial + acak ← PAKAI INI

Kenapa jitter (unsur acak) wajib: kalau seratus worker-mu terkena rate limit pada detik yang sama, backoff tanpa jitter membuat semuanya mengulang pada detik yang sama pula — dan langsung kena limit lagi. Ini disebut thundering herd. Menyebar waktu ulang secara acak memutus siklus itu.

3. retry — error mana yang layak diulang

from tenacity import retry_if_exception_type, retry_if_result

retry=retry_if_exception_type(httpx.TimeoutException)
retry=retry_if_exception_type((httpx.TimeoutException, httpx.ConnectError))
retry=retry_if_result(lambda r: r.status_code >= 500)

Yang boleh dan tidak boleh diulang

KondisiUlangi?Alasan
Timeout / koneksi putus✅Gangguan sementara
429 rate limit✅Tunggu, lalu coba lagi
500, 502, 503, 529✅Masalah di sisi server
400 bad request❌Request-mu memang salah
401 / 403❌Kredensial salah
404❌Resource tidak ada
422 validasi❌Data tidak valid

Mengulang error 4xx adalah bug yang mahal. Untuk API LLM, kamu tetap dibebani token input pada tiap percobaan — jadi mengulang request yang pasti gagal berarti membayar berkali-kali untuk kegagalan yang sama.

Pola lengkap untuk panggilan LLM

import httpx
from tenacity import (
    retry, stop_after_attempt, wait_random_exponential,
    retry_if_exception_type, before_sleep_log,
)
import logging

logger = logging.getLogger(__name__)


def layak_diulang(e: BaseException) -> bool:
    if isinstance(e, (httpx.TimeoutException, httpx.ConnectError)):
        return True
    if isinstance(e, httpx.HTTPStatusError):
        return e.response.status_code == 429 or e.response.status_code >= 500
    return False


@retry(
    stop=stop_after_attempt(5),
    wait=wait_random_exponential(multiplier=1, max=60),
    retry=retry_if_exception_type(Exception) & retry_if_exception(layak_diulang),
    before_sleep=before_sleep_log(logger, logging.WARNING),
    reraise=True,
)
async def panggil_llm(prompt: str) -> str:
    r = await client.post("/v1/messages", json={"prompt": prompt})
    r.raise_for_status()
    return r.json()["content"]

Bentuk yang lebih sederhana dan sama efektifnya:

from tenacity import retry_if_exception

@retry(
    stop=stop_after_attempt(5),
    wait=wait_random_exponential(multiplier=1, max=60),
    retry=retry_if_exception(layak_diulang),
    reraise=True,
)
async def panggil_llm(prompt: str) -> str: ...

Opsi lain yang berguna

@retry(
    reraise=True,          # lempar exception ASLI, bukan RetryError pembungkus
    before_sleep=before_sleep_log(logger, logging.WARNING),   # catat tiap percobaan
    after=after_log(logger, logging.INFO),
)
def f(): ...

reraise=True hampir selalu yang kamu mau. Tanpa itu, tenacity membungkus kegagalan terakhir dalam RetryError — sehingga blok except httpx.HTTPStatusError di pemanggil tidak akan menangkapnya lagi.

Mendengarkan header Retry-After

from tenacity import wait_base

class TungguSesuaiHeader(wait_base):
    def __call__(self, retry_state) -> float:
        e = retry_state.outcome.exception()
        if isinstance(e, httpx.HTTPStatusError):
            ra = e.response.headers.get("retry-after")
            if ra and ra.isdigit():
                return float(ra)
        return wait_random_exponential(multiplier=1, max=60)(retry_state)

Server yang baik memberi tahu berapa lama kamu harus menunggu. Menghormatinya lebih sopan dan lebih cepat pulih.

Tanpa dekorator

from tenacity import AsyncRetrying, stop_after_attempt, wait_random_exponential

async for percobaan in AsyncRetrying(
    stop=stop_after_attempt(3),
    wait=wait_random_exponential(max=30),
    reraise=True,
):
    with percobaan:
        hasil = await panggil_api()

Bentuk ini berguna kalau parameter retry-nya perlu ditentukan saat runtime.

Catatan: SDK sering sudah punya retry

SDK Anthropic sudah mengulang otomatis untuk 408, 409, 429, dan 5xx — default max_retries=2. Jadi:

client = anthropic.Anthropic(max_retries=5)     # cukup ubah angkanya

Pakai tenacity untuk lapisan di luar SDK: HTTP client-mu sendiri, panggilan ke vector store, atau logika retry tingkat aplikasi yang lebih kaya. Jangan menumpuk retry di atas retry tanpa sadar — 5 percobaan SDK × 5 percobaan tenacity = 25 panggilan berbayar.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.