Edge’de Önbellekleme: Cloudflare Workers Cache API Kullanımı

Cloudflare Workers ile edge computing dünyasına adım attığınızda, ilk karşılaştığınız güç araçlardan biri Cache API’dir. Geleneksel CDN önbellekleme yaklaşımlarının aksine, Workers Cache API size önbellekleme davranışı üzerinde tam programatik kontrol verir. Hangi içeriğin önbelleğe alınacağına, ne kadar süre tutulacağına, hangi koşullarda bypass edileceğine siz karar verirsiniz. Bu yazıda, Cache API’yi gerçek dünya senaryolarıyla derinlemesine inceleyeceğiz.

Cache API Nedir ve Neden Önemlidir?

Cloudflare Workers, her bir edge lokasyonunda çalışır. Kullanıcı isteği, coğrafi olarak en yakın Cloudflare PoP’una (Point of Presence) yönlenir ve Worker kodunuz orada çalışır. Cache API ise bu edge lokasyonundaki önbellek deposuna erişmenizi sağlar.

Standart Cloudflare CDN önbelleklemesi, HTTP yanıt başlıklarına bakarak otomatik çalışır. Cache API ise bunu bir üst seviyeye taşır: bir Worker içinden caches.default veya özel isimli önbelleklere erişebilir, istediğiniz yanıtı istediğiniz anahtarla saklayabilirsiniz.

Neden bu kadar önemli?

  • Dinamik içeriği önbelleğe alma: Kullanıcıya özel olmayan ama sık değişmeyen API yanıtlarını cache’lemek için mükemmel
  • Cache key manipülasyonu: Query string parametrelerini normalize edebilir, gereksiz varyasyonları ortadan kaldırabilirsiniz
  • Koşullu önbellekleme: Belirli header’lara, kullanıcı durumuna veya içerik tipine göre önbellekleme kararı verebilirsiniz
  • Cache bypass mantığı: Authenticated istekler, POST requestler veya belirli cookie’ler varsa cache’i atlayabilirsiniz

Temel Cache API Kullanımı

Workers’ta Cache API’nin en basit hali şöyledir:

// En temel cache-then-fetch pattern
export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;

    // Önce cache'e bak
    let response = await cache.match(request);

    if (response) {
      // Cache hit: dogrudan donelim
      return response;
    }

    // Cache miss: origin'e git
    response = await fetch(request);

    // Yaniti clone'la ve cache'e yaz
    // (Response body sadece bir kez okunabilir)
    ctx.waitUntil(cache.put(request, response.clone()));

    return response;
  }
};

Burada dikkat edilmesi gereken kritik nokta response.clone() kullanımıdır. Response objesi bir stream olduğundan, aynı body’yi hem cache’e yazmak hem de kullanıcıya göndermek için clone’lamanız gerekir. Ayrıca ctx.waitUntil() ile cache yazma işlemini arka plana alıyoruz, böylece kullanıcı cache yazma işleminin bitmesini beklemek zorunda kalmıyor.

Cache Key Özelleştirme

Gerçek dünyada en sık karşılaşılan problem, URL’lerin gereksiz varyasyonlarının cache’i parçalamasıdır. Şöyle bir senaryo düşünün: e-ticaret sitenizde ürün listesi API’niz şu şekilde çağrılıyor:

  • /api/products?sort=name&page=1
  • /api/products?page=1&sort=name
  • /api/products?page=1&sort=name&utm_source=email

Bu üçü aynı içeriği döndürür ama farklı URL’ler nedeniyle cache’de üç farklı giriş oluşur. Cache API ile bunu normalize edebilirsiniz:

export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;
    const url = new URL(request.url);

    // Sadece onemli parametreleri al, sirala
    const relevantParams = ['sort', 'page', 'category', 'limit'];
    const normalizedParams = new URLSearchParams();

    relevantParams.forEach(param => {
      if (url.searchParams.has(param)) {
        normalizedParams.set(param, url.searchParams.get(param));
      }
    });

    // UTM parametreleri ve diger tracking kodlarini at
    url.search = normalizedParams.toString();

    // Normalize edilmis URL'yi cache key olarak kullan
    const cacheKey = new Request(url.toString(), request);

    let response = await cache.match(cacheKey);

    if (response) {
      const newResponse = new Response(response.body, response);
      newResponse.headers.set('X-Cache-Status', 'HIT');
      return newResponse;
    }

    // Origin'den gercek URL ile cek
    const originalResponse = await fetch(request);

    if (originalResponse.ok) {
      const responseToCache = new Response(originalResponse.body, originalResponse);
      responseToCache.headers.set('Cache-Control', 'public, max-age=300');

      ctx.waitUntil(cache.put(cacheKey, responseToCache.clone()));
    }

    const finalResponse = new Response(originalResponse.body, originalResponse);
    finalResponse.headers.set('X-Cache-Status', 'MISS');
    return finalResponse;
  }
};

Özel İsimli Önbellekler

caches.default dışında, caches.open() ile isimlendirilmiş önbellekler açabilirsiniz. Bu, farklı içerik tipleri için farklı önbellekleme stratejileri uygulamak istediğinizde işe yarar:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Farkli icerik turleri icin farkli cache'ler
    let cacheName = 'default-cache';

    if (url.pathname.startsWith('/api/')) {
      cacheName = 'api-cache';
    } else if (url.pathname.match(/.(jpg|png|webp|gif)$/)) {
      cacheName = 'image-cache';
    } else if (url.pathname.startsWith('/static/')) {
      cacheName = 'static-assets';
    }

    const cache = await caches.open(cacheName);
    let response = await cache.match(request);

    if (response) {
      return response;
    }

    response = await fetch(request);

    // Icerik turune gore TTL belirle
    const ttlMap = {
      'api-cache': 60,        // 1 dakika
      'image-cache': 86400,   // 1 gun
      'static-assets': 604800 // 1 hafta
    };

    const ttl = ttlMap[cacheName] || 300;

    const cachedResponse = new Response(response.body, {
      status: response.status,
      headers: {
        ...Object.fromEntries(response.headers),
        'Cache-Control': `public, max-age=${ttl}`,
        'X-Cache-Name': cacheName
      }
    });

    ctx.waitUntil(cache.put(request, cachedResponse.clone()));

    return cachedResponse;
  }
};

Koşullu Önbellekleme: Authentication ve Cookie Yönetimi

En kritik pratik senaryo, authenticated kullanıcılar ile anonymous kullanıcıları ayırt etmektir. Oturum açmış kullanıcının kişisel verilerini asla cache’e yazmamalısınız, ama aynı endpoint’teki genel verileri cache’leyebilirsiniz:

export default {
  async fetch(request, env, ctx) {
    // POST, PUT, DELETE isteklerini hic cache'leme
    if (request.method !== 'GET' && request.method !== 'HEAD') {
      return fetch(request);
    }

    const url = new URL(request.url);
    const cookie = request.headers.get('Cookie') || '';
    const authHeader = request.headers.get('Authorization');

    // Authenticated istekleri bypass et
    if (authHeader || cookie.includes('session_id=') || cookie.includes('auth_token=')) {
      const response = await fetch(request);
      const newResponse = new Response(response.body, response);
      newResponse.headers.set('X-Cache-Status', 'BYPASS-AUTH');
      return newResponse;
    }

    // Admin paneli hic cache'lenmesin
    if (url.pathname.startsWith('/admin') || url.pathname.startsWith('/dashboard')) {
      return fetch(request);
    }

    const cache = caches.default;

    // Cache key'den cookie ve authorization header'i temizle
    const cacheRequest = new Request(request.url, {
      method: request.method,
      headers: {
        'Accept': request.headers.get('Accept') || '*/*',
        'Accept-Encoding': request.headers.get('Accept-Encoding') || 'gzip',
        'Accept-Language': request.headers.get('Accept-Language') || 'tr-TR'
      }
    });

    let response = await cache.match(cacheRequest);

    if (response) {
      const hit = new Response(response.body, response);
      hit.headers.set('X-Cache-Status', 'HIT');
      return hit;
    }

    response = await fetch(request);

    // Basarili yaniti cache'le
    if (response.status === 200) {
      const headers = new Headers(response.headers);
      headers.set('Cache-Control', 'public, max-age=120, stale-while-revalidate=60');
      headers.set('X-Cache-Status', 'MISS');

      // Set-Cookie iceren yanitleri cache'leme
      if (!headers.has('Set-Cookie')) {
        const toCache = new Response(response.clone().body, {
          status: response.status,
          headers
        });
        ctx.waitUntil(cache.put(cacheRequest, toCache));
      }
    }

    return response;
  }
};

Stale-While-Revalidate Pattern

Bu pattern, önbellekteki eski veriyi kullanıcıya göndirirken arka planda yeni veriyi çeken bir stratejidir. Kullanıcı latency yaşamaz, önbellek de taze tutulur:

export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;
    const cacheKey = request;

    const cachedResponse = await cache.match(cacheKey);

    if (cachedResponse) {
      const age = cachedResponse.headers.get('X-Cache-Age');
      const cachedAt = cachedResponse.headers.get('X-Cached-At');
      const maxAge = 300; // 5 dakika
      const staleMax = 600; // 10 dakika

      const ageSeconds = cachedAt
        ? (Date.now() - parseInt(cachedAt)) / 1000
        : parseInt(age || '0');

      if (ageSeconds < maxAge) {
        // Taze, direkt don
        return cachedResponse;
      }

      if (ageSeconds < staleMax) {
        // Bayat ama kabul edilebilir: eski veriyi don, arka planda yenile
        ctx.waitUntil(refreshCache(request, cache, cacheKey));

        const staleResponse = new Response(cachedResponse.body, cachedResponse);
        staleResponse.headers.set('X-Cache-Status', 'STALE');
        return staleResponse;
      }
    }

    // Cache yok veya cok eski: origin'den cek
    return refreshCache(request, cache, cacheKey);
  }
};

async function refreshCache(request, cache, cacheKey) {
  const response = await fetch(request);

  if (response.ok) {
    const headers = new Headers(response.headers);
    headers.set('X-Cached-At', Date.now().toString());
    headers.set('X-Cache-Status', 'MISS');

    const toCache = new Response(response.clone().body, {
      status: response.status,
      headers
    });

    await cache.put(cacheKey, toCache);
  }

  return response;
}

Cache Purge: Programatik Önbellek Temizleme

Bir içerik güncellendiğinde önbelleği temizlemek zorundasınız. Cache API ile bunu da programatik yapabilirsiniz:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Purge endpoint'i: POST /cache-purge
    if (url.pathname === '/cache-purge' && request.method === 'POST') {
      return handlePurge(request, env);
    }

    // Normal cache servisi
    return serveWithCache(request, ctx);
  }
};

async function handlePurge(request, env) {
  // Basit API key kontrolu
  const apiKey = request.headers.get('X-Purge-Key');
  if (apiKey !== env.PURGE_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }

  let body;
  try {
    body = await request.json();
  } catch {
    return new Response('Invalid JSON', { status: 400 });
  }

  const urls = body.urls || [];
  const cache = caches.default;
  const results = [];

  for (const targetUrl of urls) {
    try {
      const deleted = await cache.delete(new Request(targetUrl));
      results.push({ url: targetUrl, purged: deleted });
    } catch (err) {
      results.push({ url: targetUrl, purged: false, error: err.message });
    }
  }

  return new Response(JSON.stringify({ results }), {
    headers: { 'Content-Type': 'application/json' }
  });
}

async function serveWithCache(request, ctx) {
  const cache = caches.default;
  let response = await cache.match(request);

  if (!response) {
    response = await fetch(request);
    if (response.ok) {
      ctx.waitUntil(cache.put(request, response.clone()));
    }
  }

  return response;
}

Purge API’nizi şöyle test edebilirsiniz:

# Belirli URL'leri purge et
curl -X POST https://your-worker.workers.dev/cache-purge 
  -H "Content-Type: application/json" 
  -H "X-Purge-Key: your-secret-key" 
  -d '{"urls": [
    "https://your-worker.workers.dev/api/products",
    "https://your-worker.workers.dev/api/categories"
  ]}'

Gerçek Dünya Senaryosu: Haber Sitesi Edge Cache

Bir haber sitesi düşünelim. Ana sayfa ve kategori sayfaları sık değişiyor, arşiv sayfaları ise nadiren. Ayrıca editörler içerik güncellediklerinde cache’in hemen temizlenmesini istiyor:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // TTL kurallarini belirle
    function getCacheTTL(pathname) {
      if (pathname === '/' || pathname === '/index') {
        return { maxAge: 60, stale: 120 }; // Ana sayfa: 1dk
      }
      if (pathname.startsWith('/haberler/') && pathname.split('/').length === 3) {
        return { maxAge: 300, stale: 600 }; // Kategori: 5dk
      }
      if (pathname.match(//haberler/d{4}/d{2}//)) {
        return { maxAge: 3600, stale: 7200 }; // Arsiv: 1 saat
      }
      if (pathname.startsWith('/yazar/')) {
        return { maxAge: 1800, stale: 3600 }; // Yazar sayfasi: 30dk
      }
      return { maxAge: 300, stale: 600 }; // Default
    }

    // Statik assetleri direkt gecir
    if (url.pathname.match(/.(css|js|woff2|ico)$/)) {
      return fetch(request);
    }

    const { maxAge, stale } = getCacheTTL(url.pathname);
    const cache = caches.default;

    // Cache key: URL + Accept-Language (TR/EN icerik farki icin)
    const lang = request.headers.get('Accept-Language')?.startsWith('tr') ? 'tr' : 'en';
    const cacheUrl = new URL(request.url);
    cacheUrl.searchParams.set('_lang', lang);
    const cacheKey = new Request(cacheUrl.toString());

    const cached = await cache.match(cacheKey);

    if (cached) {
      const cachedAt = parseInt(cached.headers.get('X-Cached-At') || '0');
      const age = (Date.now() - cachedAt) / 1000;

      if (age < maxAge) {
        const r = new Response(cached.body, cached);
        r.headers.set('X-Cache', `HIT age=${Math.round(age)}s`);
        return r;
      }

      if (age < stale) {
        ctx.waitUntil(
          fetch(request).then(fresh => {
            if (fresh.ok) {
              const h = new Headers(fresh.headers);
              h.set('X-Cached-At', Date.now().toString());
              h.set('Cache-Control', `public, max-age=${maxAge}`);
              return cache.put(cacheKey, new Response(fresh.body, { status: 200, headers: h }));
            }
          })
        );
        const r = new Response(cached.body, cached);
        r.headers.set('X-Cache', `STALE age=${Math.round(age)}s`);
        return r;
      }
    }

    const fresh = await fetch(request);

    if (fresh.ok) {
      const headers = new Headers(fresh.headers);
      headers.set('X-Cached-At', Date.now().toString());
      headers.set('Cache-Control', `public, max-age=${maxAge}`);
      headers.set('X-Cache', 'MISS');

      const toCache = new Response(fresh.clone().body, { status: fresh.status, headers });
      ctx.waitUntil(cache.put(cacheKey, toCache));

      const resp = new Response(fresh.body, { status: fresh.status, headers });
      return resp;
    }

    return fresh;
  }
};

Dikkat Edilmesi Gereken Noktalar

Cache API kullanırken sık yapılan hatalar ve önemli kısıtlamalar şunlardır:

  • Sadece HTTPS desteklenir: Cache API yalnızca HTTPS URL’leri kabul eder. HTTP URL’leri için hata alırsınız
  • Response body bir kez okunur: Her zaman response.clone() kullanın, aksi halde “Body already used” hatası alırsınız
  • Lokasyon bazlı cache: Önbellek her PoP’ta ayrıdır, bir lokasyonda cache’lenen içerik diğer lokasyonda yoktur
  • Workers paid plan gereksinimi: caches.open() ile özel isimli önbellekler yalnızca paid plan’da çalışır
  • Cache süresi: Cache-Control header’ı olmayan veya no-store içeren yanıtlar cache’lenmez, bunu manuel override etmeniz gerekir
  • Maksimum response boyutu: Tek bir cache girdisi 512 MB ile sınırlıdır
  • waitUntil zorunluluğu: Background cache yazma işlemlerini mutlaka ctx.waitUntil() içine alın, aksi halde Worker kapanmadan önce işlem tamamlanmayabilir

Wrangler ile Test Etme

Local geliştirme ortamında Cache API davranışını test etmek için:

# Wrangler ile local dev
npx wrangler dev worker.js

# Baska terminalde test
curl -v http://localhost:8787/api/products

# Ikinci istekte HIT gormeli
curl -v http://localhost:8787/api/products

# Cache header'larini kontrol et
curl -I http://localhost:8787/api/products | grep -i "x-cache"

# Deploy et
npx wrangler deploy

# Production'da log izle
npx wrangler tail

Sonuç

Cloudflare Workers Cache API, edge önbelleklemeyi gerçek anlamda programatik hale getiriyor. Standart CDN kurallarıyla çözemediğiniz, özel iş mantığı gerektiren her türlü önbellekleme senaryosunda bu araç size tam kontrol sunuyor.

Pratik olarak en değerli kullanım alanları şunlardır: normalize edilmiş cache key’lerle hit oranını artırmak, authenticated ve anonymous trafiği doğru şekilde ayırt etmek, içerik tipine göre farklı TTL stratejileri uygulamak ve stale-while-revalidate ile kullanıcı deneyimini bozmadan önbelleği taze tutmak.

Bu yazıda anlattığım pattern’leri kendi projenize adapte ederken, her zaman X-Cache-Status gibi custom header’larla önbellek davranışını izleyin. Cache hit oranını Cloudflare Analytics’ten takip edin ve origin yükünüzün nasıl düştüğünü gözlemleyin. Düzgün implemente edilmiş bir Workers cache stratejisi, origin sunucunuza gelen trafiği ciddi ölçüde azaltır ve küresel kullanıcılarınıza tutarlı düşük latency sağlar.

Bir yanıt yazın

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