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.
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.code | Kapsam | Ne zaman oluşur? | Doğru tepki |
|---|---|---|---|
rate_limit_exceeded | API anahtarı | Kısa bir zaman penceresinde çok fazla istek gönderildiğinde | Retry-After kadar bekleyip yeniden deneyin |
quota_exceeded | Müşteri hesabı | Günlük veya aylık paket kotası dolduğunda | resetAt 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ğinde | Aynı 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ğinday).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:
Retry-Aftervarsa ona uyun. Kendi tahmininizle süreyi kısaltmayın.- 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.
- 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.
quota_exceeded,401ve403durumları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-LimitveX-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-RemainingveX-Quota-Reset: Hesabınızın api kotasıyla ilgili bilgileri taşır.X-Quota-ResetUnix 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ı
cursorile gelir. Önbellek anahtarınacursorvelimitdeğerlerini de ekleyin. - Autocomplete sorguları için kısa süreli önbellek ve istemcide gecikmeli gönderim (debounce) kullanın.
qen az üç karakter olmalıdır; daha kısa sorgular400dö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
- 429 yanıtlarını
error.codeile ayırın;messagemetnine güvenmeyin. rate_limit_exceedediçinRetry-Afterkadar bekleyin. Başlık yoksa jitter'lı backoff uygulayın ve deneme sayısını sınırlayın.quota_exceedediçin yeniden denemeyi durdurun veresetAtzamanını kaydedin.X-Quota-RemainingveX-RateLimit-Remainingdeğerlerini izleyin. Başlıklar yoksa sessizce devam edin.- Liste uçlarını önbelleğe alın ve özdeş istekleri birleştirin.
- 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.