Blog · Entegrasyon ve mimari

429 Hatası Nasıl Ele Alınır? Hız Sınırı ve Kota

429 hatası aldığınızda rate_limit_exceeded ile quota_exceeded farkını ayırın, Retry-After başlığına uyun ve gereksiz istekleri önbellekle azaltın.

4 dk okuma

Trafik arttığında bir API'den alabileceğiniz en yaygın yanıtlardan biri 429 hatasıdır. "Too Many Requests" anlamına gelen bu durum kodu, API Türkiye'de birbirinden çok farklı iki durumu karşılar. Birincisi kısa süreli bir hız sınırıdır ve birkaç saniye beklemek yeterlidir. İkincisi paket kotasının dolmasıdır ve beklemek bu durumu çözmez. İkisini aynı kodla ele alırsanız ya gereksiz yere hata gösterirsiniz ya da hiç sonuç vermeyecek yeniden denemelerle sisteminizi meşgul edersiniz. Bu yazıda iki türü nasıl ayıracağınızı, Retry-After başlığına nasıl uyacağınızı ve kalan hakkınızı nasıl izleyeceğinizi çalışan örneklerle anlatıyoruz.

429 hatası tek bir durum değildir

Aynı HTTP durumunun ardında farklı nedenler bulunur. Ayrımı yanıt gövdesindeki error.code alanı yapar:

error.codeKapsamNe zaman oluşur?Doğru tepki
rate_limit_exceededAPI anahtarıKısa bir zaman penceresinde çok fazla istek gönderildiğindeRetry-After kadar bekleyip yeniden deneyin
quota_exceededMüşteri hesabıGünlük veya aylık paket kotası dolduğundaresetAt zamanına kadar durun ya da paketinizi gözden geçirin
download_limit_exceededİmzalı indirme bağlantısıSnapshot bağlantısının kullanım hakkı bittiğindeAynı bağlantıyı denemek yerine yeni bir indirme bağlantısı isteyin

Rate limit anahtar bazında uygulanır. Kota ise hesap bazındadır: aynı hesaptaki tüm anahtarlar ortak kotayı kullanır. Bu nedenle bir servis için yeni bir anahtar oluşturmak kotayı artırmaz. Anahtarları servis bazında ayırmak yine de iyi bir uygulamadır. Bu konudaki ilkeleri API anahtarı güvenliği yazısında bulabilirsiniz.

error.code ile iki türü ayırın

Hata yanıtları her zaman error.code ve error.message alanlarını içerir. message alanı kullanıcıya okunabilir bir açıklama sunar, ancak metni değişebilir. Kodunuzdaki kararları her zaman HTTP durumuna ve error.code değerine göre verin.

quota_exceeded yanıtında karar vermenize yardımcı olacak ek alanlar bulunur:

  • period: Kotanın dönemi (örneğin day).
  • limit: Paketinize tanımlı kota değeri. Sınırı kodunuza sabit olarak yazmak yerine bu alandan ve müşteri panelinden okuyun.
  • resetAt: Kotanın yenileneceği zaman, ISO 8601 biçiminde.

Kotalar UTC takvimine göre yenilenir. Günlük kota ertesi gün 00:00 UTC'de (03:00 TSİ), aylık kota ise sonraki ayın ilk günü aynı saatte sıfırlanır. Gece yarısı Türkiye saatiyle toplu iş çalıştıran sistemler bu üç saatlik farkı hesaba katmalıdır.

Retry-After başlığına uyun

rate_limit_exceeded yanıtı Retry-After başlığıyla gelir. Bu başlık kaç saniye beklemeniz gerektiğini söyler. Bu süreden önce yapılan her deneme büyük olasılıkla yine reddedilir ve yalnızca gecikmeyi uzatır.

İyi bir yeniden deneme stratejisi şu kurallara uyar:

  1. Retry-After varsa ona uyun. Kendi tahmininizle süreyi kısaltmayın.
  2. Başlık yoksa üstel geri çekilme (exponential backoff) uygulayın ve rastgele sapma (jitter) ekleyin. Böylece aynı anda reddedilen istemcilerin tekrar aynı anda gelmesini önlersiniz.
  3. Deneme sayısına ve toplam bekleme süresine üst sınır koyun. Bir kullanıcı isteğini uzun süre bekletmek yerine anlamlı bir hata döndürmek çoğu zaman daha iyidir.
  4. quota_exceeded, 401 ve 403 durumlarında otomatik yeniden deneme yapmayın. Bu durumlar ancak anahtar, yetki veya paket düzeltildiğinde değişir.

Başlıkları görmek için yanıtı başlıklarıyla birlikte yazdırabilirsiniz:

curl -i "https://api.apiturkiye.com/v1/provinces" -H "X-API-Key: apt_live_..."

Dayanıklı yeniden deneme: JavaScript

Aşağıdaki sunucu tarafı fonksiyonu iki 429 türünü ayırır. Hız sınırında ve geçici 503 catalog_unavailable durumunda bekleyip yeniden dener. Kota dolduğunda ise ayrı bir hata sınıfı fırlatır.

const BASE = "https://api.apiturkiye.com";
const MAX_ATTEMPTS = 4;
const MAX_WAIT_MS = 30_000;

export class QuotaExceededError extends Error {
  constructor(detail) {
    super("API kotası doldu");
    this.name = "QuotaExceededError";
    this.period = detail.period;
    this.limit = detail.limit;
    this.resetAt = detail.resetAt;
  }
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

function backoffMs(attempt) {
  const cap = Math.min(1000 * 2 ** attempt, MAX_WAIT_MS);
  return Math.floor(Math.random() * cap); // tam jitter
}

function retryAfterMs(res) {
  const value = res.headers.get("Retry-After");
  if (!value) return null;
  const seconds = Number(value);
  return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
}

export async function apiGet(path, apiKey = process.env.APITURKIYE_KEY) {
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
    const res = await fetch(`${BASE}${path}`, { headers: { "X-API-Key": apiKey } });
    if (res.ok) return res.json();

    const body = await res.json().catch(() => ({}));
    const code = body.error?.code;

    if (res.status === 429 && code === "quota_exceeded") {
      throw new QuotaExceededError(body.error);
    }

    const retryable =
      (res.status === 429 && code === "rate_limit_exceeded") ||
      (res.status === 503 && code === "catalog_unavailable");

    if (retryable && attempt < MAX_ATTEMPTS - 1) {
      const wait = retryAfterMs(res) ?? backoffMs(attempt);
      if (wait <= MAX_WAIT_MS) {
        await sleep(wait);
        continue;
      }
    }

    const err = new Error(`HTTP ${res.status} ${code ?? ""}`.trim());
    err.status = res.status;
    err.code = code;
    throw err;
  }
}

Kullanım:

try {
  const { data } = await apiGet("/v1/provinces/34/districts");
} catch (err) {
  if (err instanceof QuotaExceededError) {
    console.warn(`Kota doldu, yenilenme: ${err.resetAt}`);
  } else {
    throw err;
  }
}

Aynı mantık Python ile

import os
import random
import time

import requests

BASE = "https://api.apiturkiye.com"
MAX_ATTEMPTS = 4
MAX_WAIT_S = 30

class QuotaExceeded(Exception):
    def __init__(self, error):
        super().__init__("API kotası doldu")
        self.period = error.get("period")
        self.limit = error.get("limit")
        self.reset_at = error.get("resetAt")

def retry_after_seconds(res):
    value = res.headers.get("Retry-After")
    try:
        return max(0.0, float(value)) if value else None
    except ValueError:
        return None

def backoff(attempt):
    return random.uniform(0, min(2 ** attempt, MAX_WAIT_S))

session = requests.Session()
session.headers["X-API-Key"] = os.environ["APITURKIYE_KEY"]

def api_get(path, params=None):
    for attempt in range(MAX_ATTEMPTS):
        res = session.get(f"{BASE}{path}", params=params, timeout=10)
        if res.ok:
            return res.json()

        try:
            error = res.json().get("error", {})
        except ValueError:
            error = {}
        code = error.get("code")

        if res.status_code == 429 and code == "quota_exceeded":
            raise QuotaExceeded(error)

        retryable = (res.status_code, code) in {
            (429, "rate_limit_exceeded"),
            (503, "catalog_unavailable"),
        }
        if retryable and attempt < MAX_ATTEMPTS - 1:
            wait = retry_after_seconds(res)
            if wait is None:
                wait = backoff(attempt)
            if wait <= MAX_WAIT_S:
                time.sleep(wait)
                continue

        res.raise_for_status()

X-Quota başlıklarıyla kalan hakkı izleyin

429 hatası almayı beklemek yerine kalan hakkınızı önceden izleyebilirsiniz. İki başlık grubu bu iş için kullanılır:

  • X-RateLimit-Limit ve X-RateLimit-Remaining: Anahtarınızın geçerli hız penceresindeki durumunu gösterir. Kalan değer düştüğünde toplu işleri yavaşlatabilirsiniz.
  • X-Quota-Limit, X-Quota-Remaining ve X-Quota-Reset: Hesabınızın api kotasıyla ilgili bilgileri taşır. X-Quota-Reset Unix saniyesi biçimindedir.

Kota başlıklarının her uçta veya her hata yanıtında bulunacağını varsaymayın. Başlık yoksa kodunuz sessizce devam etmelidir:

export function trackQuota(res, warnAt = Number(process.env.QUOTA_WARN_AT ?? 0)) {
  const remaining = res.headers.get("X-Quota-Remaining");
  if (remaining === null) return; // bu yanıtta kota başlığı yok

  const reset = res.headers.get("X-Quota-Reset");
  const resetAt = reset ? new Date(Number(reset) * 1000).toISOString() : "bilinmiyor";

  if (Number(remaining) <= warnAt) {
    console.warn(`Kota azalıyor: kalan=${remaining}, yenilenme=${resetAt}`);
  }
}

Bu fonksiyonu apiGet içinde her fetch çağrısından sonra çalıştırıp değeri metrik sisteminize gönderebilirsiniz. Eşik değerini paketinize göre ortam değişkeninden okumak, kodu değiştirmeden ayar yapmanızı sağlar.

İki ayrıntıya dikkat edin. Birincisi, 429 ile reddedilen çağrılar kullanım kotasından düşülmez. İkincisi, bu kural diğer hata türleri için geçerli olmayabilir. Hatalı parametreyle yapılan istekleri kotadan muaf saymayın ve bunları istemci tarafında önleyin.

Gereksiz istekleri önbellekle azaltın

En etkili 429 önlemi, hiç gönderilmesi gerekmeyen istekleri göndermemektir. Adres verisinin büyük bölümü sık değişmez:

  • İl, ilçe ve mahalle listeleri ilgili kapsamın tamamını tek yanıtta döner. Bunları uzun süreli önbelleğe almak uygundur.
  • Cadde/sokak ve dış kapı sayfaları cursor ile gelir. Önbellek anahtarına cursor ve limit değerlerini de ekleyin.
  • Autocomplete sorguları için kısa süreli önbellek ve istemcide gecikmeli gönderim (debounce) kullanın. q en az üç karakter olmalıdır; daha kısa sorgular 400 döner, bu yüzden onları hiç göndermeyin. Ayrıntılar için adres otomatik tamamlama yazısına bakabilirsiniz.

Aynı anda gelen özdeş istekleri tek bir çağrıda birleştirmek de önemlidir. Örneğin yüzlerce kullanıcı aynı anda İstanbul ilçelerini istediğinde API'ye yalnızca bir istek gitmelidir:

const cache = new Map();    // path -> { value, expiresAt }
const inflight = new Map(); // path -> Promise

export async function cachedGet(path, ttlMs) {
  const hit = cache.get(path);
  if (hit && hit.expiresAt > Date.now()) return hit.value;
  if (inflight.has(path)) return inflight.get(path);

  const promise = apiGet(path)
    .then((value) => {
      cache.set(path, { value, expiresAt: Date.now() + ttlMs });
      return value;
    })
    .finally(() => inflight.delete(path));

  inflight.set(path, promise);
  return promise;
}

const DAY = 24 * 60 * 60 * 1000;
const { data: districts } = await cachedGet("/v1/provinces/34/districts", DAY);

Birden fazla sunucu çalıştırıyorsanız bellek içi önbellek yerine paylaşılan bir önbellek kullanın. Paketinizde datasets:read yetkisi varsa /v1/datasets/current yanıtındaki datasetVersion değeri değiştiğinde önbelleği yenileyebilirsiniz. Böylece süreyi tahmin etmek yerine veri sürümüne bağlı bir yenileme yapmış olursunuz.

Frontend tarafında ne yapmalı?

API anahtarı tarayıcı koduna konmaz; tarayıcı yalnızca kendi backend'inizle konuşur. Bu yüzden 429 yönetiminin asıl yeri sunucudur. Frontend için şu kurallar yeterlidir:

  • Backend'iniz kota dolduğunda anlamlı bir yanıt döndürsün. Arayüz bu durumda "Adres önerileri şu an kullanılamıyor, adresi elle girebilirsiniz" gibi bir alternatif sunsun.
  • İstek sürerken gönder düğmesini pasif hâle getirin ve otomatik yenileme döngülerinden kaçının.
  • Tarayıcıda ikinci bir yeniden deneme katmanı kurmayın. Sunucu zaten yeniden deniyorsa iki katman, istek sayısını katlayarak artırır.

Kısa kontrol listesi

  1. 429 yanıtlarını error.code ile ayırın; message metnine güvenmeyin.
  2. rate_limit_exceeded için Retry-After kadar bekleyin. Başlık yoksa jitter'lı backoff uygulayın ve deneme sayısını sınırlayın.
  3. quota_exceeded için yeniden denemeyi durdurun ve resetAt zamanını kaydedin.
  4. X-Quota-Remaining ve X-RateLimit-Remaining değerlerini izleyin. Başlıklar yoksa sessizce devam edin.
  5. Liste uçlarını önbelleğe alın ve özdeş istekleri birleştirin.
  6. Beklenmedik artışlarda önce servis durumu sayfasını kontrol edin.

Uçların ve hata kodlarının tam listesine API dokümantasyonu üzerinden ulaşabilirsiniz. Henüz bir anahtarınız yoksa ücretsiz kayıt ile başlayabilirsiniz. Kullanım hacminiz büyüdüğünde paket karşılaştırması bölümünden size uygun seçeneği inceleyebilirsiniz.