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: private olması gereken yanıtlar public olarak 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: true ve Sunset: Sat, 01 Jan 2025 00:00:00 GMT headerları 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.
  • Vary header’ının doğru yapılandırılarak cache fragmentation’a yol açmaması.
  • ETag’lerin veritabanı düzeyinde, updated_at timestamp’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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir