Buforowanie na krawędzi: poza tradycyjnym CDN

Tradycyjna sieć CDN buforuje zasoby statyczne zgodnie ze standardowymi nagłówkami HTTP. Pracownicy Cloudflare posuwa tę koncepcję znacznie dalej: za pomocą Pamięć podręczna API, Twój kod TypeScript kontroluje z chirurgiczną precyzją, co i jak długo jest buforowane, w jaki sposób jest ona unieważniana i jaką logikę zastępczą zastosować, gdy pamięć podręczna jest nieaktualna.

Wynik jest jeden Programowalny CDN: zamiast konfigurować reguły statyczny w dashboardzie, napisz logikę biznesową, która na bieżąco będzie decydować, czy a odpowiedź jest buforowana, z jakim TTL i jakim kluczem pamięci podręcznej. Ta elastyczność jest to szczególnie cenne w przypadku interfejsów API REST, stron niestandardowych i treści półdynamicznych.

Czego się nauczysz

  • Jak działa API Cloudflare Workers Cache i czym różni się od API Cache przeglądarki
  • Strategie buforowania: Cache-First, Network-First, Stale-While-Revalidate
  • Niestandardowe klucze pamięci podręcznej: izoluj pamięć podręczną według użytkownika, języka i wersji
  • Unieważnianie adresów URL, tagów i prefiksów za pomocą interfejsu API Cloudflare Zone Purge
  • Zmieniaj nagłówki: zróżnicowana pamięć podręczna dla kodowania akceptacji i języka akceptacji
  • Zaawansowane wzorce: podgrzewanie pamięci podręcznej, okres karencji i wyłącznik automatyczny z KV
  • Typowe błędy i jak ich uniknąć na produkcji

Interfejs API pamięci podręcznej: podstawy i różnice w stosunku do przeglądarki

Interfejs API pamięci podręcznej udostępniony w programie Workers ma ten sam interfejs, co interfejs Interfejs API pamięci podręcznej Service Worker przeglądarkę, ale z istotnymi różnicami. W Workers pamięć podręczna jest dystrybuowane we wszystkich punktach PoP Cloudflare: Kiedy Robotnik we Frankfurcie buforuje odpowiedź, ta odpowiedź nie jest automatycznie dostępna w Londynie i Amsterdamie. Każdy PoP ma własną lokalną pamięć podręczną.

// Accesso alla cache nel Worker
// In Workers esiste un unico "default" cache namespace
const cache = caches.default;

// Oppure cache named (isolata per nome, utile per namespace logici)
const apiCache = await caches.open('api-v2');

// Le operazioni fondamentali:
// cache.match(request) -> Response | undefined
// cache.put(request, response) -> void
// cache.delete(request) -> boolean

Cache API: ważne ograniczenia

  • Tylko żądania HTTP (nie dowolne adresy URL, jak w przeglądarce)
  • Nie można buforować odpowiedzi w Vary: *
  • Pamięć podręczna jest obsługiwana przez PoP: nie ma automatycznego unieważniania między centrami danych cache.delete()
  • Odpowiedzi o statusie 206 (Częściowa treść) nie są buforowane
  • Maksymalny przestrzegany TTL wynosi 31 dni

Strategia 1: Najpierw pamięć podręczna z siecią awaryjną

Najpopularniejsza strategia dla interfejsów API z rzadko zmieniającymi się danymi: udostępniaj z pamięci podręcznej jeśli jest dostępny, w przeciwnym razie przejdź do źródła i zapełnij pamięć podręczną.

// worker.ts - Cache-First Strategy

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Solo richieste GET sono cacheable
    if (request.method !== 'GET') {
      return fetch(request);
    }

    const cache = caches.default;

    // 1. Controlla la cache
    let response = await cache.match(request);

    if (response) {
      // Cache HIT: aggiungi header diagnostico e restituisci
      const headers = new Headers(response.headers);
      headers.set('X-Cache-Status', 'HIT');
      return new Response(response.body, {
        status: response.status,
        headers,
      });
    }

    // 2. Cache MISS: fetch dall'origin
    response = await fetch(request);

    // 3. Clona la risposta (il body e un ReadableStream, consumabile una sola volta)
    const responseToCache = response.clone();

    // 4. Metti in cache con ctx.waitUntil() per non bloccare la risposta al client
    ctx.waitUntil(
      cache.put(request, responseToCache)
    );

    // 5. Restituisci la risposta originale con header diagnostico
    const headers = new Headers(response.headers);
    headers.set('X-Cache-Status', 'MISS');
    return new Response(response.body, {
      status: response.status,
      headers,
    });
  },
};

interface Env {}

Strategia 2: Ponowna weryfikacja na bieżąco

Strategia nieaktualne podczas ponownej weryfikacji to ten, który daje najlepsze kompromis pomiędzy świeżością danych a postrzeganą szybkością: klient otrzymuje Zawsze natychmiastową odpowiedź z pamięci podręcznej, nawet jeśli działa ona w tle Worker aktualizuje pamięć podręczną dla następnego żądania.

// Stale-While-Revalidate implementato manualmente
// (Cloudflare supporta anche il header standard, ma questa versione offre più controllo)

const CACHE_TTL = 60;        // Secondi prima che la cache sia "fresh"
const STALE_TTL = 300;       // Secondi aggiuntivi in cui la cache e "stale but usable"

interface CacheMetadata {
  cachedAt: number;
  ttl: number;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    if (request.method !== 'GET') return fetch(request);

    const cache = caches.default;

    // Crea una Request con una custom cache key che include i metadata
    const cacheKey = new Request(request.url, {
      headers: request.headers,
    });

    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 < CACHE_TTL) {
        // FRESH: servi dalla cache senza revalidazione
        return addCacheHeaders(cached, 'FRESH', age);
      }

      if (age < CACHE_TTL + STALE_TTL) {
        // STALE: servi dalla cache ma revalida in background
        ctx.waitUntil(revalidate(cacheKey, cache));
        return addCacheHeaders(cached, 'STALE', age);
      }
    }

    // MISS o troppo vecchio: fetch sincrono
    return fetchAndCache(cacheKey, cache, ctx);
  },
};

async function revalidate(cacheKey: Request, cache: Cache): Promise<void> {
  const fresh = await fetch(cacheKey.url);
  if (fresh.ok) {
    const toCache = addTimestamp(fresh);
    await cache.put(cacheKey, toCache);
  }
}

async function fetchAndCache(
  cacheKey: Request,
  cache: Cache,
  ctx: ExecutionContext
): Promise<Response> {
  const response = await fetch(cacheKey.url);

  if (response.ok) {
    const toCache = addTimestamp(response.clone());
    ctx.waitUntil(cache.put(cacheKey, toCache));
  }

  const headers = new Headers(response.headers);
  headers.set('X-Cache-Status', 'MISS');
  return new Response(response.body, { status: response.status, headers });
}

function addTimestamp(response: Response): Response {
  const headers = new Headers(response.headers);
  headers.set('X-Cached-At', String(Date.now()));
  // Cache-Control: max-age elevato per far "sopravvivere" la risposta in cache
  headers.set('Cache-Control', 'public, max-age=86400');
  return new Response(response.body, { status: response.status, headers });
}

function addCacheHeaders(response: Response, status: string, age: number): Response {
  const headers = new Headers(response.headers);
  headers.set('X-Cache-Status', status);
  headers.set('Age', String(Math.floor(age)));
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Niestandardowe klucze pamięci podręcznej: izoluj pamięć podręczną według kontekstu

Domyślnie kluczem pamięci podręcznej jest pełny adres URL żądania. Ale często potrzebujesz pamięci podręcznych rozdzielonych według języka, wersji API, poziomu użytkownika lub urządzenia. Rozwiązaniem jest zbuduj jeden niestandardowy klucz pamięci podręcznej jako przedmiot Request z krótkim adresem URL.

// Cache differenziata per lingua e versione API

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const cache = caches.default;
    const url = new URL(request.url);

    // Estrai parametri rilevanti per la cache key
    const lang = request.headers.get('Accept-Language')?.split(',')[0]?.split('-')[0] ?? 'en';
    const apiVersion = url.searchParams.get('v') ?? 'v1';
    const tier = request.headers.get('X-User-Tier') ?? 'free';

    // Costruisci una URL sintetica come cache key
    // Non deve essere una URL reale, solo identificativa
    const cacheKeyUrl = new URL(request.url);
    cacheKeyUrl.searchParams.set('_ck_lang', lang);
    cacheKeyUrl.searchParams.set('_ck_v', apiVersion);
    // Non includiamo 'tier' nella key se vuoi condividere la cache tra tier

    const cacheKey = new Request(cacheKeyUrl.toString(), {
      method: 'GET',
      // Importante: non copiare headers di autenticazione nella cache key
      // altrimenti ogni utente avrebbe la propria cache entry
    });

    // Cerca nella cache con la custom key
    let response = await cache.match(cacheKey);

    if (response) {
      return response;
    }

    // Fetch dall'origin passando la richiesta originale (con auth headers)
    const originResponse = await fetch(request);

    if (originResponse.ok && isCacheable(originResponse)) {
      const toCache = setCacheHeaders(originResponse.clone(), 300);
      ctx.waitUntil(cache.put(cacheKey, toCache));
    }

    return originResponse;
  },
};

function isCacheable(response: Response): boolean {
  // Non mettere in cache risposte con dati personali o Set-Cookie
  if (response.headers.has('Set-Cookie')) return false;
  if (response.headers.get('Cache-Control')?.includes('private')) return false;
  if (response.headers.get('Cache-Control')?.includes('no-store')) return false;
  return true;
}

function setCacheHeaders(response: Response, maxAge: number): Response {
  const headers = new Headers(response.headers);
  headers.set('Cache-Control', `public, max-age=${maxAge}, s-maxage=${maxAge}`);
  // Rimuovi header che potrebbero impedire il caching
  headers.delete('Set-Cookie');
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Nagłówki pamięci podręcznej: s-maxage, nieaktualne podczas ponownego sprawdzania poprawności, nieaktualne w przypadku błędu

Cloudflare szanuje Standardowe nagłówki Cache-Control i przedłuża go znaczenie. Zrozumienie tych wytycznych jest niezbędne:

Dyrektywa Oznaczający Przykład
max-age=N TTL dla przeglądarki i CDN (N sekund) max-age=300
s-maxage=N TTL tylko dla CDN/proxy (zastępuje maksymalny wiek dla Cloudflare) s-maxage=3600, max-age=60
stale-while-revalidate=N Dodatkowe sekundy, w ciągu których można podać nieaktualne podczas ponownej walidacji s-maxage=60, stale-while-revalidate=300
stale-if-error=N Sekundy, w których ma zostać wyświetlony nieaktualny, jeśli źródło zwróci błąd stale-if-error=86400
no-store W żadnym wypadku nie buforuj Dla wrażliwych danych
private Tylko przeglądarka pamięci podręcznej, a nie CDN Dla uwierzytelnionych odpowiedzi
// Esempio: API con s-maxage e stale-while-revalidate via header

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Routing con TTL differenziati per tipo di risorsa
    if (url.pathname.startsWith('/api/products')) {
      return fetchWithCacheHeaders(request, {
        sMaxAge: 300,           // 5 minuti fresh in CDN
        staleWhileRevalidate: 3600, // 1 ora stale accettabile
        staleIfError: 86400,    // 1 giorno stale in caso di errore origin
      });
    }

    if (url.pathname.startsWith('/api/user')) {
      // Dati utente: non cacheare in CDN
      return fetchWithCacheHeaders(request, {
        sMaxAge: 0,
        private: true,
      });
    }

    if (url.pathname.startsWith('/static')) {
      // Asset statici: cache aggressiva
      return fetchWithCacheHeaders(request, {
        sMaxAge: 31536000, // 1 anno
        immutable: true,
      });
    }

    return fetch(request);
  },
};

interface CacheOptions {
  sMaxAge?: number;
  staleWhileRevalidate?: number;
  staleIfError?: number;
  private?: boolean;
  immutable?: boolean;
}

async function fetchWithCacheHeaders(
  request: Request,
  options: CacheOptions
): Promise<Response> {
  const response = await fetch(request);
  const headers = new Headers(response.headers);

  let cacheControl = '';

  if (options.private) {
    cacheControl = 'private, no-store';
  } else {
    const parts: string[] = ['public'];
    if (options.sMaxAge !== undefined) parts.push(`s-maxage=${options.sMaxAge}`);
    if (options.staleWhileRevalidate) parts.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
    if (options.staleIfError) parts.push(`stale-if-error=${options.staleIfError}`);
    if (options.immutable) parts.push('immutable');
    cacheControl = parts.join(', ');
  }

  headers.set('Cache-Control', cacheControl);
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Unieważnienie: Usuń adres URL, tag i prefiks

cache.delete(request) usuń pamięć podręczną tylko w lokalnym punkcie PoP, w którym działa Worker. Aby unieważnić pamięć podręczną we wszystkich punktach PoP na całym świecie, musisz skorzystać z API REST usuwania strefy Cloudflare. Jest to właściwy mechanizm zarządzania treścią i wdrożyć.

// Invalidation globale tramite Cloudflare API
// Da usare tipicamente da un webhook CMS o da un Worker admin

interface PurgeOptions {
  files?: string[];      // URL specifici
  tags?: string[];       // Cache-Tag headers
  prefixes?: string[];   // URL prefix
  hosts?: string[];      // Tutti gli URL di un host
}

async function purgeCloudflareCache(
  zoneId: string,
  apiToken: string,
  options: PurgeOptions
): Promise<void> {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(options),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Purge failed: ${JSON.stringify(error)}`);
  }
}

// Worker che funge da webhook per invalidazione CMS
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    // Verifica il secret del webhook
    const secret = request.headers.get('X-Webhook-Secret');
    if (secret !== env.WEBHOOK_SECRET) {
      return new Response('Unauthorized', { status: 401 });
    }

    const body = await request.json() as WebhookPayload;

    // Invalida le URL specifiche aggiornate dal CMS
    if (body.type === 'post.updated') {
      await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
        files: [
          `https://example.com/blog/${body.slug}`,
          `https://example.com/api/posts/${body.id}`,
          `https://example.com/sitemap.xml`,
        ],
      });
    }

    // Invalida per tag (richiede Cache-Tag header sulle risposte origin)
    if (body.type === 'category.updated') {
      await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
        tags: [`category-${body.categorySlug}`],
      });
    }

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

interface WebhookPayload {
  type: string;
  id?: string;
  slug?: string;
  categorySlug?: string;
}

interface Env {
  ZONE_ID: string;
  CF_API_TOKEN: string;
  WEBHOOK_SECRET: string;
}

Tagi pamięci podręcznej: unieważnienie semantyczne

I Tagi pamięci podręcznej są najpotężniejszym mechanizmem unieważniania selektywny. Działają poprzez dodanie nagłówka Cache-Tag do odpowiedzi: każda odpowiedź może mieć wiele tagów i możesz unieważnić wszystkie adresy URL powiązany z tagiem za pomocą pojedynczego wywołania API.

// Origin server o Worker che aggiunge Cache-Tag alle risposte

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const response = await fetch(request);
    const headers = new Headers(response.headers);

    // Aggiungi tag semantici basati sul contenuto
    const tags: string[] = [];

    // Tag per tipo di risorsa
    if (url.pathname.startsWith('/api/products')) {
      const productId = url.pathname.split('/')[3];
      tags.push('products');                    // Invalida tutti i prodotti
      if (productId) tags.push(`product-${productId}`); // Invalida questo prodotto specifico
    }

    if (url.pathname.startsWith('/api/categories')) {
      const catId = url.pathname.split('/')[3];
      tags.push('categories');
      if (catId) tags.push(`category-${catId}`);
    }

    // Tag per versione dell'API
    const apiVersion = url.pathname.split('/')[2];
    if (apiVersion?.startsWith('v')) {
      tags.push(`api-${apiVersion}`);
    }

    if (tags.length > 0) {
      // Cache-Tag: lista separata da virgole, max 16KB
      headers.set('Cache-Tag', tags.join(','));
    }

    return new Response(response.body, { status: response.status, headers });
  },
};

// Esempio di invalidazione per tag dopo un aggiornamento:
// POST /api/admin/purge
// { "tags": ["product-123", "categories"] }
// Invalida tutte le URL che hanno Cache-Tag: product-123 o categories

interface Env {}

Zaawansowany wzorzec: pamięć podręczna z KV jako L2

Interfejs API pamięci podręcznej ma ważne ograniczenie: nie jest dostępny programowo dla dowolnego odczytu/zapisu, tylko żądania HTTP. Więcej wzorów złożone (takie jak skoordynowane unieważnianie, wyłącznik automatyczny lub pamięć podręczna obiektów). inne niż HTTP), użyj Workers KV jako pamięć podręczna L2.

// Cache a due livelli: Cache API (L1, HTTP) + KV (L2, programmabile)

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const cacheKey = buildCacheKey(url);

    // L1: Cache API (piu veloce, locale al PoP)
    const l1Cache = caches.default;
    const l1Hit = await l1Cache.match(request);

    if (l1Hit) {
      return addHeader(l1Hit, 'X-Cache', 'L1-HIT');
    }

    // L2: KV Store (globale, programmabile, con TTL gestito da KV)
    const kvCached = await env.API_CACHE.getWithMetadata<CacheMetadata>(cacheKey);

    if (kvCached.value !== null) {
      const { value, metadata } = kvCached;

      // Ricostruisci una Response dalla stringa KV
      const cachedResponse = new Response(value, {
        headers: {
          'Content-Type': metadata?.contentType ?? 'application/json',
          'Cache-Control': 'public, max-age=60',
          'X-Cache': 'L2-HIT',
          'X-Cached-At': String(metadata?.cachedAt ?? 0),
        },
      });

      // Popola anche L1 per richieste successive nello stesso PoP
      ctx.waitUntil(l1Cache.put(request, cachedResponse.clone()));

      return cachedResponse;
    }

    // MISS su entrambi i livelli: fetch dall'origin
    const originResponse = await fetch(request);

    if (originResponse.ok) {
      const body = await originResponse.text();
      const contentType = originResponse.headers.get('Content-Type') ?? 'application/json';

      const metadata: CacheMetadata = {
        cachedAt: Date.now(),
        contentType,
        url: url.toString(),
      };

      // Salva in KV con TTL di 5 minuti
      ctx.waitUntil(
        env.API_CACHE.put(cacheKey, body, {
          expirationTtl: 300,
          metadata,
        })
      );

      // Salva anche in L1
      const toL1 = new Response(body, {
        headers: {
          'Content-Type': contentType,
          'Cache-Control': 'public, max-age=60',
          'X-Cache': 'MISS',
        },
      });
      ctx.waitUntil(l1Cache.put(request, toL1));

      return new Response(body, {
        headers: {
          'Content-Type': contentType,
          'X-Cache': 'MISS',
        },
      });
    }

    return addHeader(originResponse, 'X-Cache', 'BYPASS-ERROR');
  },
};

function buildCacheKey(url: URL): string {
  // Normalizza l'URL per la cache key (rimuovi query params non semantici)
  const params = new URLSearchParams(url.searchParams);
  params.delete('utm_source');
  params.delete('utm_medium');
  params.delete('_t'); // timestamp di cache-busting
  params.sort(); // ordine deterministico
  return `${url.pathname}?${params.toString()}`;
}

function addHeader(response: Response, key: string, value: string): Response {
  const headers = new Headers(response.headers);
  headers.set(key, value);
  return new Response(response.body, { status: response.status, headers });
}

interface CacheMetadata {
  cachedAt: number;
  contentType: string;
  url: string;
}

interface Env {
  API_CACHE: KVNamespace;
}

Ogrzewanie pamięci podręcznej: Wstępnie wypełnij pamięć podręczną

Il podgrzewanie pamięci podręcznej jest to praktyka polegająca na wstępnym wypełnieniu pamięci podręcznej że przychodzą prawdziwe żądania, eliminując problem zimnej pamięci podręcznej po wdrożeniu masowe unieważnienia. Jest implementowany za pomocą pracownika zaplanowanego za pomocą Cron Trigger.

// wrangler.toml - Cron Trigger per cache warming
// [triggers]
// crons = ["*/15 * * * *"]  # Ogni 15 minuti

// worker.ts - Cache Warming Worker

const URLS_TO_WARM = [
  'https://api.example.com/products?featured=true',
  'https://api.example.com/categories',
  'https://api.example.com/homepage',
  'https://api.example.com/navigation',
];

export default {
  // Scheduled handler per Cron Trigger
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
    console.log(`Cache warming started at ${new Date(event.scheduledTime).toISOString()}`);

    const results = await Promise.allSettled(
      URLS_TO_WARM.map(url => warmUrl(url))
    );

    const succeeded = results.filter(r => r.status === 'fulfilled').length;
    const failed = results.filter(r => r.status === 'rejected').length;

    console.log(`Cache warming complete: ${succeeded} success, ${failed} failed`);
  },

  async fetch(request: Request): Promise<Response> {
    return new Response('Cache Warmer Worker', { status: 200 });
  },
};

async function warmUrl(url: string): Promise<void> {
  // Forza bypass della cache aggiungendo header speciale
  // (da gestire lato Worker principale con whitelist IP o secret)
  const response = await fetch(url, {
    headers: {
      'Cache-Control': 'no-cache', // Forza revalidazione
      'X-Cache-Warm': 'true',
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to warm ${url}: ${response.status}`);
  }
}

interface Env {}
interface ScheduledEvent {
  scheduledTime: number;
  cron: string;
}

Debugowanie i monitorowanie pamięci podręcznej

Cloudflare udostępnia nagłówki diagnostyczne w odpowiedziach, aby zrozumieć stan pamięci podręcznej. Najważniejsze jest CF-Cache-Status:

Stan pamięci podręcznej CF Oznaczający
HIT Obsługiwane z pamięci podręcznej Cloudflare
MISS Nie buforowane, wymagane w miejscu pochodzenia
EXPIRED Zapisano w pamięci podręcznej, ale wygasł czas TTL, żądanie do źródła
STALE Podawane jako nieświeże (nieaktualne do ponownego sprawdzenia)
BYPASS Pominięto pamięć podręczną (plik cookie, nagłówek uwierzytelniania itp.)
DYNAMIC Niebuforowalne (odpowiedź dynamiczna)
REVALIDATED Pamięć podręczna sprawdzona w Origin (304 niemodyfikowana)
// Worker che logga le metriche di cache su KV Analytics
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const response = await fetch(request);

    const cacheStatus = response.headers.get('CF-Cache-Status') ?? 'UNKNOWN';

    // Logga la metrica in background
    ctx.waitUntil(
      logCacheMetric(env, {
        url: request.url,
        status: cacheStatus,
        timestamp: Date.now(),
        country: request.cf?.country ?? 'unknown',
        datacenter: request.cf?.colo ?? 'unknown',
      })
    );

    return response;
  },
};

async function logCacheMetric(env: Env, metric: CacheMetric): Promise<void> {
  // Aggrega per finestre di 1 minuto
  const minuteKey = `metrics:${Math.floor(metric.timestamp / 60000)}:${metric.status}`;
  const current = parseInt(await env.METRICS_KV.get(minuteKey) ?? '0');
  await env.METRICS_KV.put(minuteKey, String(current + 1), { expirationTtl: 3600 });
}

interface CacheMetric {
  url: string;
  status: string;
  timestamp: number;
  country: string;
  datacenter: string;
}

interface Env {
  METRICS_KV: KVNamespace;
}

Wnioski i dalsze kroki

API Cloudflare Workers Cache API przekształca CDN z narzędzia pasywnego w komponent aktywny w architekturze. Dzięki strategiom opisanym w tym artykule możesz budować ziarnista, semantyczna, nieprawidłowa warstwa pamięci podręcznej, która zmniejsza obciążenie 70–90% pochodzenia w przypadku typowych obciążeń publicznych interfejsów API.

Kluczowe punkty do zapamiętania: użyj ctx.waitUntil() aby nie blokować odpowiedzi do klienta, zbuduj niestandardowe klucze pamięci podręcznej, aby odizolować różne konteksty, użyj znaczników pamięci podręcznej dla unieważnienia semantycznego i połącz Cache API z KV, aby uzyskać bardziej złożone wzorce.

Następne artykuły z serii

  • Artykuł 9: Testowanie pracowników lokalnych — Miniflare, Vitest e Wrangler Dev: jak pisać testy jednostkowe i testy integracyjne dla pracowników bez wdrażania.
  • Artykuł 10: Architektury Full-Stack na krawędzi — studium przypadku autorstwa Od zera do produkcji: kompletny interfejs API REST z modułami Workers + D1 + R2 i CI/CD w akcjach GitHub.