API response caching backend engineering’in en basit gibi görünen, en sık berbat edilen konularından biri. HTTP spec’i yıllardır hazır cevabı veriyor: ETag, Last-Modified, Cache-Control. Ama pek çok API bunları yanlış kullanıyor veya hiç kullanmıyor.
Son 3 projede API caching’e ciddi yatırım yaptım. Ortak pattern’lar ve tuzaklar.
Ne zaman cache anlamlı
Cache bandwidth + CPU + latency üçlüsünü kazandırıyor. Anlamlı olduğu durumlar:
- Read-heavy endpoint: product catalog, article listesi, kullanıcı profile
- Expensive computation: aggregate, ML inference, full-text search
- Rate-limited upstream: third-party API çağrısı
- Common response: tüm kullanıcılar aynı data görüyor
Cache anlamsız olduğu durumlar:
- Write-heavy (her update cache invalidate ediyor)
- Per-user unique response
- Real-time data (stock price, notification count)
- Small response + fast generation
3 cache layer
Response cache genelde 3 seviyede:
- Browser/client cache: client local cache
- CDN/reverse proxy: edge cache (Cloudflare, Varnish, NGINX)
- Application cache: Redis, Memcached, in-memory
Her layer farklı key yönetimi, farklı invalidation stratejisi.
ETag: the honest way
ETag (entity tag) response body’nin hash’i veya versiyonu. Client bir response aldığında ETag’i sakliyor, bir sonraki request’te If-None-Match: header’ı ile gönderiyor.
Server ETag hala geçerliyse 304 Not Modified döner, body yok. Client cached versiyonu kullanır.
# İlk request
GET /api/products/42
Response:
200 OK
ETag: "abc123"
Body: {...}
# İkinci request (cache'li)
GET /api/products/42
If-None-Match: "abc123"
Response:
304 Not Modified
(no body)ETag generation stratejileri:
Content hash: response body’nin SHA-1/MD5 hash’i. Kesin ama hesaplama maliyeti var (her request’te hash).
Version field: kaynak objesinin version veya updated_at field’ı. DB query’ye dayanıyor.
ID + timestamp combination: "{id}-{updated_at.timestamp}". Hızlı, deterministic.
def generate_etag(product):
return f'"{product.id}-{product.updated_at.timestamp()}"'Last-Modified: alternative
ETag yerine/yanında Last-Modified header. Client If-Modified-Since ile kontrol ediyor.
Response:
200 OK
Last-Modified: Wed, 20 Apr 2026 10:00:00 GMT
Subsequent request:
If-Modified-Since: Wed, 20 Apr 2026 10:00:00 GMT
Response:
304 Not ModifiedETag’e göre farkları:
– Granularity: saniye (ETag’de arbitrary precision)
– Comparison: timestamp (ETag’de exact match)
– Clock skew sensitivity var (server/client saat farkı)
Ben ETag’i tercih ediyorum, daha robust. Last-Modified legacy support için.
Cache-Control: TTL-based caching
TTL-based cache: response belirli süre geçerli kabul ediliyor, expire olana kadar revalidation gerekmiyor.
Cache-Control: public, max-age=300, s-maxage=600public: tüm cache’ler (browser, CDN) cache edebilirprivate: sadece client browser cachemax-age=300: browser 5 dakika caches-maxage=600: CDN 10 dakika cache (shared cache)
max-age ile ETag birlikte kullanılabiliyor. Max-age geçinceye kadar fresh, geçince revalidation (ETag).
stale-while-revalidate: modern pattern
Cache-Control: max-age=60, stale-while-revalidate=3600Response 60 saniye fresh. Sonrasında 1 saat boyunca stale gösterilebilir, background’da revalidation yapılabiliyor.
Kullanıcı deneyimi: cache miss yok, her istekte hızlı response (muhtemelen biraz stale), background’da güncelleniyor. Sonraki request fresh.
Bu pattern performance + consistency trade-off’unu iyi çözüyor. Modern CDN’ler (Cloudflare, Fastly) destekliyor.
Cache invalidation: zor problem
Cache invalidation “2 zor problemden biri” deniyor. Strategies:
1. TTL-based. En basit. Cache belirli süre sonra expire. Eventual consistency kabul ediyorsunuz.
2. Explicit purge. Write sonrası cache key’leri explicitly invalidate. Cloudflare API, Varnish ban command, Redis DEL.
def update_product(product_id, data):
db.update(product_id, data)
cache.delete(f"product:{product_id}")
cdn.purge(f"/api/products/{product_id}")3. Versioning in key. Cache key’e version ekle, version değiştirdiğinizde eski key ignore ediliyor.
product:v3:42Version bump = tam invalidation. Granular invalidation zor.
4. Event-driven invalidation. Write event yayınla (Kafka, pub/sub), cache workers subscribed, invalidation async.
Büyük sistemlerde scalable. Eventual consistency accepted.
Per-user response problemi
User-specific response cache’lemek karmaşık. Vary header kullanımı:
Cache-Control: private, max-age=60
Vary: Authorization, Accept-LanguageVary: Authorization demek cache key’e auth token’ı da dahil, her user ayrı cache entry. Public cache için sınırlı etki (authenticated user’lar farklı entry).
Per-user cache daha etkili olduğu durum: Redis user-specific cache, key’de user_id.
cache.set(f"user:{user_id}:dashboard", response, ex=60)Conditional GET discipline
Client ETag’i kullanmayı unutabiliyor. SDK/library yazıyorsanız otomatik ETag handling ekleyin:
class APIClient:
def __init__(self):
self.etag_cache = {}
def get(self, path):
headers = {}
if path in self.etag_cache:
headers['If-None-Match'] = self.etag_cache[path]
response = requests.get(self.base_url + path, headers=headers)
if response.status_code == 304:
return self.response_cache[path]
self.etag_cache[path] = response.headers.get('ETag')
self.response_cache[path] = response.json()
return response.json()Monitoring: cache hit rate
Cache effectiveness’i ölçmek için:
- Cache hit rate: toplam request’in % kaç cache’ten servis edildi
- Cache miss latency: cache miss olan request’lerin p95 latency
- Cache size: memory usage
- Evict rate: cache full olduğunda ne sıklıkta eviction
Hit rate %80 altındaysa cache effective değil, TTL düşük veya key strategy yanlış olabilir.
Custom cache: Redis direct
Bazen standard HTTP caching yetmiyor. Complex query result’ları, aggregated data. Redis direkt:
def get_user_dashboard(user_id):
cache_key = f"dashboard:{user_id}"
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
dashboard = compute_dashboard(user_id) # expensive
redis.setex(cache_key, 300, json.dumps(dashboard))
return dashboardInvalidation: user data değişince redis.delete(f"dashboard:{user_id}").
Anti-patterns
1. Cache-aside’de race condition. İki request aynı anda cache miss, ikisi de compute ediyor, cache’e yazıyor. Thundering herd.
Mitigation: lock (Redis SETNX pattern), veya background refresh.
2. Permanent cache. TTL = sonsuz. Data değişince asla invalidate olmuyor. Stale data permanent.
3. No max size. Redis/Memcached memory dolu, OOM. Her cache’in maxmemory policy’si olmalı (LRU, LFU).
4. Cache stampede. Popular key expire oldu, 1000 concurrent request aynı anda miss, hepsi compute ediyor. DB down.
Mitigation: probabilistic early expiration, random jitter, lock.
Gerçek sonuçlar
Bir e-ticaret API’sinde cache stratejisi:
Before: her request DB query, p95 280ms
After: ETag + CDN + Redis, p95 45ms, %87 cache hit rate
Cost azaldı (DB load düştü, worker sayısı yarı), latency iyileşti, user experience daha iyi.
Son tavsiye
API caching’i başta çalışırken, production’a çıkarken her endpoint için caching stratejisi belirleyin. “Cache sonra eklerim” approach’u 6 ay sonra retrofit etmek zor oluyor, çünkü cache strategy API design’ına etki ediyor.
HTTP standard cache mechanism’larını (ETag, Cache-Control) ciddiye alın. Custom cache ancak gerekli olduğunda. Minimum viable cache > complex custom cache.