โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 8 ยท Cloudflare: Cache, Edge & WAF

Purge selektif dengan Cache Tag

Purge Everything pada portal dengan 500 ribu request per jam berarti mengirim seluruh trafik itu ke origin sekaligus. Purge by tag menghindarinya sepenuhnya.

Intisari

  • Purge by tag dan by prefix tersedia di semua plan sejak April 2025; yang berbeda hanya rate limit-nya.
  • Header Cache-Tag dari origin memberi label pada respons; satu respons boleh punya banyak tag.
  • Maksimum 100 tag per permintaan purge, dan panjang total header Cache-Tag dibatasi.
  • Jangan pernah Purge Everything di jam sibuk โ€” origin-mu akan menerima seluruh trafik sekaligus.
  • Purge harus dipanggil setelah transaksi database commit, bukan di dalamnya.

Memberi label

// Di halaman artikel
Astro.response.headers.set(
  "Cache-Tag",
  [
    `artikel-${artikel.id}`,
    `kategori-${artikel.kategoriSlug}`,
    `penulis-${artikel.penulisId}`,
    "tipe-artikel",
  ].join(","),
);
// Di halaman kategori โ€” tag yang beririsan, disengaja
Astro.response.headers.set(
  "Cache-Tag",
  [`kategori-${slug}`, "tipe-daftar", "beranda-terkait"].join(","),
);

Irisan tag inilah yang membuat sistemnya bekerja. Saat artikel disunting, purge artikel-4471 membersihkan halaman artikelnya; purge kategori-ekonomi sekaligus membersihkan halaman kategori dan beranda yang menampilkannya.

Memanggil purge

// src/lib/purge.ts
import { env } from "./env";

export async function purgeTag(tag: string[]): Promise<boolean> {
  if (tag.length === 0) return true;

  // Maksimum 100 tag per permintaan.
  const potongan: string[][] = [];
  for (let i = 0; i < tag.length; i += 100) potongan.push(tag.slice(i, i + 100));

  for (const p of potongan) {
    try {
      const res = await fetch(
        `https://api.cloudflare.com/client/v4/zones/${env.CF_ZONE_ID}/purge_cache`,
        {
          method: "POST",
          signal: AbortSignal.timeout(8000),
          headers: {
            Authorization: `Bearer ${env.CF_API_TOKEN}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ tags: p }),
        },
      );

      if (!res.ok) {
        console.error(JSON.stringify({
          level: "error", pesan: "purge gagal", status: res.status, tag: p,
        }));
        return false;
      }
    } catch (e) {
      console.error(JSON.stringify({ level: "error", pesan: "purge error", e: String(e) }));
      return false;
    }
  }
  return true;
}

Token API-nya harus dibuat dengan izin sesempit mungkin. Buat token khusus dengan hanya Zone โ†’ Cache Purge โ†’ Purge pada satu zona. Token global API key milik akunmu bisa melakukan apa saja terhadap seluruh domainmu โ€” termasuk memindahkan DNS. Kalau container-mu dikompromikan, bedanya adalah antara "cache dibersihkan" dan "domain dibajak".

Memanggilnya di tempat yang benar

export const terbitkanArtikel = defineAction({
  input: z.object({ id: z.number().int().positive() }),
  handler: async ({ id }, ctx) => {
    if (!boleh(ctx.locals.member?.peran, "artikel:terbitkan")) {
      throw new ActionError({ code: "FORBIDDEN" });
    }

    // 1. Transaksi database dulu, dan selesai.
    const artikel = await dbTulis.transaction().execute(async (trx) => {
      await trx.updateTable("artikel")
        .set({ status: "terbit", terbit_pada: new Date() })
        .where("id", "=", id).where("status", "=", "tinjau")
        .executeTakeFirstOrThrow();

      return trx.selectFrom("artikel")
        .select(["id", "kategori_id", "penulis_id"])
        .where("id", "=", id)
        .executeTakeFirstOrThrow();
    });

    // 2. Purge SETELAH commit.
    await purgeTag([
      `artikel-${artikel.id}`,
      `kategori-${await slugKategori(artikel.kategori_id)}`,
      `penulis-${artikel.penulis_id}`,
      "beranda",
    ]);

    return { ok: true };
  },
});

Purge di dalam transaksi adalah bug yang tampak halus tapi nyata. Kalau kamu purge lalu transaksinya rollback, kamu baru saja membersihkan cache untuk perubahan yang tidak pernah terjadi โ€” dan permintaan berikutnya akan mengisi ulang cache dengan data lama. Lebih buruk: purge sebelum commit berarti ada jendela di mana cache sudah kosong tapi database belum berubah, sehingga versi lama di-cache ulang dan bertahan sampai TTL berikutnya.

Peta tag untuk portalmu

PeristiwaTag yang di-purge
Artikel disuntingartikel-{id}
Artikel diterbitkanartikel-{id}, kategori-{slug}, beranda, penulis-{id}
Artikel ditarikSama
Kategori diganti namakategori-{slug}, tipe-daftar
Data perusahaan diperbaruiperusahaan-{kode}, tipe-data
Menu situs berubahSemua โ€” pakai prefix, bukan tag
Deploy baruTidak perlu โ€” aset ber-hash punya nama baru

Kenapa Purge Everything berbahaya di trafikmu

Sebelum purge : 93% dilayani edge, ~21 req/detik ke origin
Purge Everything
Sesudah purge : 0% dilayani edge,  ~347 req/detik ke origin

Origin-mu menerima 16 kali lipat beban normalnya, seketika, sampai
cache terisi kembali. Autoscaling butuh menit untuk bereaksi.
Yang terjadi lebih dulu adalah pool database habis.

Kalau kamu benar-benar harus purge semuanya โ€” misalnya setelah mengubah layout global โ€” lakukan di jam paling sepi, naikkan jumlah task ECS lebih dulu, dan pertimbangkan memakai purge by prefix bertahap per bagian situs alih-alih sekaligus. Dan pastikan stale-while-revalidate-mu panjang, karena itu yang menahan gelombangnya.

Purge by prefix

await fetch(`https://api.cloudflare.com/client/v4/zones/${env.CF_ZONE_ID}/purge_cache`, {
  method: "POST",
  headers: { Authorization: `Bearer ${env.CF_API_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    prefixes: ["portal.contoh.id/berita/ekonomi/"],
  }),
});

Berguna untuk perubahan yang menyentuh seluruh cabang URL โ€” misalnya struktur kategori berubah. Batasnya 100 prefix per permintaan.

Kalau purge gagal

PilihanKonsekuensi
Gagalkan action-nyaEditor melihat error padahal artikel sudah terbit
Catat dan lanjutkanArtikel basi maksimal selama TTL โ€” untuk artikel 300 detik, itu bisa diterima
Antre dan coba lagiPaling benar, butuh infrastruktur antrean

Untuk portal berita, pilihan kedua hampir selalu tepat: kegagalan purge berarti pembaca melihat versi lama selama beberapa menit, sementara menggagalkan penerbitan berarti redaksi terhambat. Pasang alarm kalau tingkat kegagalan purge naik.

Latihan: pasang header Cache-Tag di halaman artikel dan kategori, lalu tulis purgeTag(). Uji urutannya: muat artikel sampai cf-cache-status: HIT, ubah judulnya di database, muat lagi (masih HIT dengan judul lama), panggil purge, lalu muat lagi โ€” harus MISS dengan judul baru. Buat token API dengan izin purge saja, dan verifikasi ia tidak bisa melakukan hal lain.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.