API Önbellekleme Stratejileri: ETag ve Cache-Control Kullanımı
Bir API’nin performansını artırmanın en etkili yollarından biri, istemci ile sunucu arasındaki gereksiz veri transferini azaltmaktır. ETag ve Cache-Control mekanizmaları, bu sorunu HTTP protokolünün kendi araçlarıyla çözmenizi sağlar. Teorik olarak basit görünen bu kavramlar, pratikte yanlış yapılandırıldığında hem güvenlik açıklarına hem de ciddi performans sorunlarına yol açabilir. Bu yazıda, gerçek dünya senaryoları üzerinden API önbellekleme stratejilerini derinlemesine inceleyeceğiz.
HTTP Önbelleklemenin Temelleri
HTTP önbellekleme, iki farklı mekanizma üzerine kuruludur:
- Expiration (Süre temelli): İstemci, belirli bir süre boyunca sunucuya hiç sormadan önbellekteki veriyi kullanır.
- Validation (Doğrulama temelli): İstemci sunucuya “Bu veri hâlâ geçerli mi?” diye sorar, sunucu ya “Evet, kullan” ya da “Hayır, işte yeni veri” der.
ETag ve Cache-Control bu iki mekanizmanın temel taşlarıdır. Gerçek dünyada çoğunlukla ikisini birlikte kullanmak gerekir çünkü birbirlerini tamamlarlar.
Cache-Control Direktifleri
Cache-Control header’ı, HTTP/1.1 ile gelen ve önbellekleme davranışını kontrol eden en güçlü araçtır. Sunucu tarafında doğru yapılandırılmadığında, özel kullanıcı verileri shared cache’lerde saklanabilir veya kritik güncellemeler istemcilere hiç ulaşmayabilir.
En sık kullanılan direktifler şunlardır:
- max-age=N: Yanıt N saniye boyunca “fresh” (taze) kabul edilir.
- s-maxage=N: Shared cache’ler (CDN, proxy) için max-age değerini override eder.
- no-cache: Yanıtı önbelleğe alabilirsin ama kullanmadan önce sunucuya doğrulat.
- no-store: Yanıtı kesinlikle önbelleğe alma, hassas veriler için kullanılır.
- private: Yanıt yalnızca browser cache’ine alınabilir, shared cache’e alınamaz.
- public: Yanıt shared cache’lere de alınabilir.
- must-revalidate: Önbellek süresi dolduktan sonra kullanmadan önce sunucuya doğrulat.
- stale-while-revalidate=N: Süresi dolmuş veriyi kullanırken arka planda güncelle.
- stale-if-error=N: Sunucu hata verirse N saniye boyunca eski veriyi kullan.
Bir e-ticaret API’si için tipik bir Cache-Control stratejisi şöyle görünebilir:
# Ürün listeleme endpoint'i - CDN'de 5 dakika, browser'da 1 dakika
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
# Kullanıcı profil verisi - sadece browser cache, 10 dakika
Cache-Control: private, max-age=600, must-revalidate
# Ödeme endpoint'i - kesinlikle cache'leme
Cache-Control: no-store
# Admin panel API'leri - önbelleğe al ama her seferinde doğrulat
Cache-Control: no-cache, private
ETag: Veri Parmak İzi
ETag (Entity Tag), bir kaynağın belirli bir versiyonunu temsil eden benzersiz bir tanımlayıcıdır. Sunucu kaynağı oluşturduğunda bir ETag değeri üretir ve bunu response header’ında gönderir. İstemci bir sonraki istekte bu değeri If-None-Match header’ında gönderir, sunucu kaynağın değişip değişmediğini kontrol eder ve değişmediyse sadece 304 Not Modified döner.
Bu mekanizma bant genişliğini dramatik biçimde azaltabilir. Özellikle büyük JSON payload’larında farkı net görürsünüz.
Güçlü ve Zayıf ETag’ler
- Güçlü ETag (
"abc123"): Kaynağın byte-by-byte aynı olduğunu garanti eder. Range request’lerle güvenle kullanılabilir. - Zayıf ETag (
W/"abc123"): Kaynağın “semantik olarak eşdeğer” olduğunu belirtir. Anlam değişmemişse ufak formatting farklılıklarını görmezden gelir.
Nginx ile Cache-Control Yapılandırması
Gerçek bir API altyapısında Nginx’i reverse proxy olarak kullanıyorsanız, Cache-Control header’larını merkezi olarak yönetmek hem tutarlılık hem de güvenlik açısından önemlidir.
# /etc/nginx/sites-available/api.example.com
server {
listen 443 ssl http2;
server_name api.example.com;
# Genel cache header güvenlik ayarları
add_header X-Cache-Status $upstream_cache_status;
location /api/v1/products {
proxy_pass http://backend:8080;
# Ürünler için public cache - CDN dostu
add_header Cache-Control "public, max-age=60, s-maxage=300, stale-while-revalidate=30";
# ETag desteğini etkinleştir
etag on;
}
location /api/v1/users {
proxy_pass http://backend:8080;
# Kullanıcı verileri private olmalı
add_header Cache-Control "private, max-age=300, must-revalidate";
# Shared cache'in bu veriyi saklamasını engelle
proxy_hide_header Cache-Control;
add_header Cache-Control "private, no-store";
}
location /api/v1/payments {
proxy_pass http://backend:8080;
# Ödeme endpoint'lerinde önbellekleme yasak
add_header Cache-Control "no-store, no-cache";
add_header Pragma "no-cache";
}
# Statik asset'ler için agresif cache
location /api/v1/static {
proxy_pass http://backend:8080;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
Node.js/Express ile ETag Implementasyonu
Express.js’de ETag desteği varsayılan olarak gelir ama özelleştirmek çoğu zaman gereklidir. Özellikle database-driven API’lerde, ETag’ı doğru kaynaktan üretmek performans açısından kritiktir.
# Önce gerekli paketleri yükleyelim
npm install express etag crypto
# server.js
cat << 'EOF' > server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
// Express'in kendi weak ETag'ini devre dışı bırakıp
// custom strong ETag kullanalım
app.set('etag', false);
// ETag middleware
function generateETag(data) {
return crypto
.createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
}
// Conditional GET middleware
function conditionalGet(req, res, next) {
res.sendWithETag = function(data, maxAge = 60) {
const etag = `"${generateETag(data)}"`;
const clientETag = req.headers['if-none-match'];
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
if (clientETag && clientETag === etag) {
// Veri değişmemiş, sadece 304 döndür
return res.status(304).end();
}
res.json(data);
};
next();
}
app.use(conditionalGet);
// Ürün endpoint'i
app.get('/api/products', async (req, res) => {
// Gerçek senaryoda DB'den gelir
const products = await getProductsFromDB();
res.sendWithETag(products, 300);
});
// Tek ürün - updated_at'i ETag olarak kullan
app.get('/api/products/:id', async (req, res) => {
const product = await getProductById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Not found' });
}
// updated_at timestamp'ini ETag basis olarak kullan
// Bu yaklaşım hash hesaplamaktan daha verimli
const etag = `"${product.id}-${product.updated_at}"`;
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, max-age=60, must-revalidate');
res.setHeader('Last-Modified', new Date(product.updated_at).toUTCString());
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(product);
});
app.listen(3000);
EOF
Python/FastAPI ile ETag ve Cache-Control
Python ekosisteminde, özellikle FastAPI kullanan takımlar için middleware bazlı bir çözüm görelim:
# requirements.txt
# fastapi==0.104.0
# uvicorn==0.24.0
cat << 'EOF' > cache_middleware.py
import hashlib
import json
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from datetime import datetime, timedelta
from functools import wraps
app = FastAPI()
def cache_response(max_age: int = 60, private: bool = False, no_store: bool = False):
"""
Decorator: Cache-Control ve ETag yönetimi
Kullanim:
@cache_response(max_age=300, private=True)
async def get_user_profile():
...
"""
def decorator(func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
response_data = await func(request, *args, **kwargs)
if no_store:
headers = {
"Cache-Control": "no-store, no-cache",
"Pragma": "no-cache"
}
return JSONResponse(content=response_data, headers=headers)
# ETag hesapla
data_str = json.dumps(response_data, sort_keys=True, default=str)
etag = f'"{hashlib.md5(data_str.encode()).hexdigest()}"'
# Cache-Control header'ı oluştur
if private:
cache_control = f"private, max-age={max_age}, must-revalidate"
else:
cache_control = f"public, max-age={max_age}, s-maxage={max_age * 5}"
# Conditional request kontrolü
client_etag = request.headers.get("if-none-match")
if client_etag and client_etag == etag:
return Response(
status_code=304,
headers={
"ETag": etag,
"Cache-Control": cache_control
}
)
return JSONResponse(
content=response_data,
headers={
"ETag": etag,
"Cache-Control": cache_control,
"Vary": "Accept-Encoding, Accept"
}
)
return wrapper
return decorator
@app.get("/api/products")
@cache_response(max_age=300)
async def get_products(request: Request):
return {"products": [...]}
@app.get("/api/users/me")
@cache_response(max_age=600, private=True)
async def get_user_profile(request: Request):
return {"user": {...}}
@app.post("/api/payments")
@cache_response(no_store=True)
async def process_payment(request: Request):
return {"status": "processed"}
EOF
CDN Entegrasyonu ve Vary Header’ı
CDN kullanıyorsanız Vary header’ı kritik önem taşır. CDN’ye “bu içeriği hangi request header’larına göre ayrı ayrı önbellekle” demiş olursunuz. Yanlış yapılandırılmış Vary header’ı hem cache hit rate’i düşürür hem de güvenlik sorunlarına yol açabilir.
# Nginx'te Vary header yönetimi
# /etc/nginx/conf.d/vary-headers.conf
# Dil bazlı cache
location /api/v1/content {
proxy_pass http://backend:8080;
# Her dil için ayrı cache entry oluştur
add_header Vary "Accept-Language, Accept-Encoding";
add_header Cache-Control "public, max-age=3600";
}
# API versiyonu bazlı cache
location /api {
proxy_pass http://backend:8080;
# Accept header'a göre (JSON vs XML) ayrı cache
add_header Vary "Accept, Accept-Encoding";
add_header Cache-Control "public, max-age=300";
# Authorization header varsa private olmalı!
# Vary: Authorization kullanmak yerine:
if ($http_authorization) {
add_header Cache-Control "private, no-store";
}
}
# Güvenli: Authorization içeren istekleri hiç cache'leme
map $http_authorization $cache_control {
"" "public, max-age=300, s-maxage=600";
default "private, no-cache";
}
location /api/v2 {
proxy_pass http://backend:8080;
add_header Cache-Control $cache_control;
}
Gerçek Dünya Senaryosu: Büyük E-ticaret API’si
Bir e-ticaret platformunda çalışırken karşılaştığım gerçek bir sorunu ele alalım. Ürün kataloğu API’si her istek için MB’larca JSON döndürüyordu ve sunucu hem bant genişliği hem de işlemci açısından bunalıyordu.
Çözüm, katmanlı bir önbellekleme stratejisiydi:
#!/bin/bash
# cache-strategy-test.sh
# Önbellekleme stratejinizin etkinliğini test etmek için
API_URL="https://api.example.com"
TOKEN="your-auth-token"
echo "=== İlk İstek (Cache Miss Bekleniyor) ==="
curl -s -D -
-H "Authorization: Bearer $TOKEN"
-H "Accept: application/json"
"${API_URL}/api/v1/products"
-o /dev/null | grep -E "HTTP|ETag|Cache-Control|X-Cache"
echo ""
echo "=== ETag'i al ve ikinci isteği yap ==="
ETAG=$(curl -s -I
-H "Authorization: Bearer $TOKEN"
"${API_URL}/api/v1/products"
| grep -i "etag:" | tr -d 'r' | awk '{print $2}')
echo "Alınan ETag: $ETAG"
echo ""
echo "=== Conditional GET (304 Bekleniyor) ==="
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer $TOKEN"
-H "If-None-Match: $ETAG"
"${API_URL}/api/v1/products")
if [ "$HTTP_STATUS" -eq 304 ]; then
echo "BAŞARILI: Sunucu 304 döndürdü, bant genişliği tasarrufu sağlandı!"
else
echo "UYARI: Sunucu $HTTP_STATUS döndürdü, ETag düzgün çalışmıyor olabilir"
fi
echo ""
echo "=== Cache-Control Güvenlik Kontrolü ==="
# Ödeme endpoint'inin kesinlikle cache'lenmemesi lazım
PAYMENT_CACHE=$(curl -s -I
-H "Authorization: Bearer $TOKEN"
"${API_URL}/api/v1/payments"
| grep -i "cache-control:")
if echo "$PAYMENT_CACHE" | grep -qi "no-store"; then
echo "GÜVENLİ: Ödeme endpoint'i no-store kullanıyor"
else
echo "TEHLİKE: Ödeme endpoint'i güvenli şekilde yapılandırılmamış!"
echo "Bulunan: $PAYMENT_CACHE"
fi
Güvenlik: Önbelleklemenin Karanlık Yüzü
Önbellekleme stratejileri yanlış uygulandığında ciddi güvenlik açıklarına neden olabilir. En sık karşılaşılan sorunlar şunlardır:
- Cache Poisoning: Saldırgan, paylaşılan önbelleğe zararlı içerik enjekte eder ve tüm kullanıcıları etkiler.
- Sensitive Data Exposure:
privateolması gereken yanıtlarpublicolarak işaretlenip CDN’de saklandığında kullanıcı verileri sızabilir. - Stale Authorization: Kullanıcının yetkisi kaldırıldığında eski veri önbellekten servis edilmeye devam edebilir.
# Güvenlik açıklarını kontrol etmek için header analiz scripti
# /usr/local/bin/check-api-cache-security.sh
#!/bin/bash
TARGET=$1
if [ -z "$TARGET" ]; then
echo "Kullanim: $0 <api-url>"
exit 1
fi
echo "API Cache Güvenlik Analizi: $TARGET"
echo "========================================"
# 1. Authorization gerektiren endpoint'lerde private kontrolü
echo "[1] Authorization endpoint cache kontrolü..."
RESPONSE=$(curl -s -D - -H "Authorization: Bearer test123"
"${TARGET}/api/v1/profile" -o /dev/null 2>&1)
if echo "$RESPONSE" | grep -qi "cache-control:.*public"; then
echo " HATA: Auth gerektiren endpoint public cache kullanıyor!"
elif echo "$RESPONSE" | grep -qi "cache-control:.*private"; then
echo " OK: private cache kullanılıyor"
else
echo " UYARI: Cache-Control header eksik veya belirsiz"
fi
# 2. Set-Cookie olan yanıtların cache'lenmemesi
echo "[2] Set-Cookie ile Cache-Control uyumu..."
HAS_COOKIE=$(curl -s -I "${TARGET}/api/v1/login" | grep -i "set-cookie")
CACHE_CTRL=$(curl -s -I "${TARGET}/api/v1/login" | grep -i "cache-control")
if [ -n "$HAS_COOKIE" ] && echo "$CACHE_CTRL" | grep -qiv "private|no-store"; then
echo " HATA: Set-Cookie içeren yanıt private/no-store değil!"
else
echo " OK: Cookie içeren yanıt güvenli şekilde işaretlenmiş"
fi
# 3. CORS ile cache uyumu
echo "[3] CORS ve Vary header uyumu..."
VARY=$(curl -s -I -H "Origin: https://evil.com"
"${TARGET}/api/v1/products" | grep -i "vary:")
if echo "$VARY" | grep -qi "origin"; then
echo " OK: Vary: Origin kullanılıyor, CORS güvenli"
else
echo " UYARI: Cross-origin istekler için Vary: Origin eksik olabilir"
fi
echo "========================================"
echo "Analiz tamamlandı."
Redis ile Sunucu Taraflı Cache ve ETag Senkronizasyonu
Gerçek bir prodüksiyon ortamında, ETag değerlerini ve önbellek geçersizleştirme (cache invalidation) mantığını yönetmek için Redis kullanmak oldukça yaygın bir yaklaşımdır:
# redis-cache-manager.sh
# ETag ve cache verilerini Redis'te yönetmek için yardımcı scriptler
REDIS_CLI="redis-cli -h redis.internal"
# Bir kaynağın ETag'ini güncelle ve eski cache'i geçersiz kıl
invalidate_resource_cache() {
local resource_type=$1
local resource_id=$2
local new_etag=$3
# Eski ETag'i güncelle
$REDIS_CLI SET "etag:${resource_type}:${resource_id}"
"$new_etag" EX 3600
# CDN purge tetikle (Cloudflare örneği)
curl -s -X POST
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache"
-H "Authorization: Bearer ${CF_API_TOKEN}"
-H "Content-Type: application/json"
--data "{
"files": [
"https://api.example.com/api/v1/${resource_type}/${resource_id}"
]
}" | jq '.success'
echo "Cache invalidated: ${resource_type}/${resource_id}"
}
# Toplu ürün güncellemesi sonrası tüm ürün cache'ini temizle
invalidate_product_category() {
local category_id=$1
# Bu kategorideki tüm ürünlerin ETag'lerini sil
$REDIS_CLI KEYS "etag:products:cat:${category_id}:*" |
xargs -r $REDIS_CLI DEL
# CDN'de kategori URL'ini temizle
curl -s -X POST
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache"
-H "Authorization: Bearer ${CF_API_TOKEN}"
-H "Content-Type: application/json"
--data "{
"tags": ["category-${category_id}"]
}"
echo "Kategori ${category_id} cache temizlendi"
}
# Örnek kullanım
# Ürün güncellendiğinde
# invalidate_resource_cache "products" "12345" "abc123def456"
# Fiyat güncellemesi sonrası
# invalidate_product_category "electronics"
Cache-Control ile API Versiyonlama
API’nizin yeni bir versiyonunu yayınladığınızda mevcut önbellekleri temizlemek kritiktir. Bu senaryoyu yönetmek için URL bazlı versiyonlama ile Cache-Control’ü birlikte kullanmak pratik bir çözümdür:
- URL’de versiyon (
/api/v2/): Her versiyon kendi cache namespace’ine sahip olur, otomatik olarak izole edilir. - ETag’e versiyon prefix’i:
"v2-abc123"formatında ETag üretmek, versiyon geçişlerinde yanlış 304 yanıtlarını önler. - Deprecation header:
Deprecation: trueveSunset: Sat, 01 Jan 2025 00:00:00 GMTheaderları ile eski versiyonları işaretleyin. İstemciler önbellekten eski versiyon yanıtı alırken bile bu uyarıyı görmeli. - Cache-Control max-age kısaltma: Eski API versiyonlarını devre dışı bırakmadan önce max-age değerini kademeli olarak düşürün (600 -> 120 -> 30 -> 0).
Sonuç
ETag ve Cache-Control, doğru uygulandığında API performansını katlayarak artırabilir. Yazdığım bir API’de bu mekanizmaları devreye aldıktan sonra bant genişliği kullanımının yüzde 60-70 düştüğünü, sunucu yanıt sürelerinin ise neredeyse yarıya indiğini bizzat gördüm.
Ancak önbellekleme bir “kur ve unut” mekanizması değildir. Özellikle dikkat etmeniz gereken noktalar:
- Kullanıcıya özel verilerin (
private) ve ödeme gibi hassas işlemlerin (no-store) asla shared cache’e düşmemesi. Varyheader’ının doğru yapılandırılarak cache fragmentation’a yol açmaması.- ETag’lerin veritabanı düzeyinde,
updated_attimestamp’i gibi verimli kaynaklardan üretilmesi. - Cache invalidation stratejisinin deployment sürecinize entegre edilmesi.
- Güvenlik taramalarını otomatik hale getirerek configuration drift’i önlemek.
Bu kavramları production’da uygulamaya başlarken küçük adımlarla ilerleyin: önce statik ve public endpointlerde deneyin, ardından dinamik ve kullanıcıya özel verilere geçin. Her adımda hem cache hit rate’i hem de güvenlik başlıklarını izleyin. Prometheus ve Grafana ile bu metrikleri görselleştirmek, stratejinizi sürekli iyileştirmenize yardımcı olacaktır.
