← Semua pembelajaran / Go Nol → Enterprise
Fase 9 · Performa & Observability

Cache HTTP & CDN — pengungkit terbesar

Untuk situs bertrafik baca tinggi, tidak ada optimasi lain yang mendekati dampak ini: halaman yang sama bagi semua pembaca cukup dihasilkan sekali per menit, sisanya dilayani CloudFront.

Intisari

  • s-maxage mengatur CDN, max-age mengatur browser — dan keduanya bisa berbeda.
  • stale-while-revalidate menyajikan versi lama seketika sambil menyegarkan di latar. Ini yang menghapus lonjakan setelah kedaluwarsa.
  • Halaman personal wajib private atau no-store. Satu kesalahan di sini menyajikan data satu pengguna ke pengguna lain.
  • ETag + If-None-Match menghasilkan 304 — hemat bandwidth meski tidak hemat kueri.
  • Aset ber-hash nama boleh immutable dan di-cache setahun.

Direktif yang benar-benar dipakai

DirektifArtinya
publicBoleh disimpan cache bersama (CDN)
privateHanya browser pengguna — CDN tidak boleh menyimpannya
no-storeJangan disimpan di mana pun
max-age=NSegar selama N detik (untuk semua cache)
s-maxage=NSegar selama N detik khusus cache bersama; menimpa max-age di CDN
stale-while-revalidate=NBoleh menyajikan yang basi sampai N detik sambil menyegarkan di latar
stale-if-error=NBoleh menyajikan yang basi kalau origin sedang error
must-revalidateJangan pernah sajikan yang basi
immutableTidak akan pernah berubah — jangan pernah validasi ulang

Resep per jenis halaman

// 1. Halaman publik yang sama untuk semua pembaca — SASARAN UTAMA.
//    Browser menyimpan 60 detik; CDN menyimpan 5 menit dan boleh
//    menyajikan versi basi sampai 1 jam sambil menyegarkan.
w.Header().Set("Cache-Control",
	"public, max-age=60, s-maxage=300, stale-while-revalidate=3600, stale-if-error=86400")

// 2. Aset ber-hash nama (styles.9ea43564.css) — nama baru untuk isi baru
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

// 3. Respons API publik, berubah cukup sering
w.Header().Set("Cache-Control", "public, max-age=0, s-maxage=30")

// 4. Halaman personal — WAJIB
w.Header().Set("Cache-Control", "private, no-store")

// 5. Apa pun yang memuat data sensitif
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")

Nomor 4 adalah baris yang paling mahal kalau salah. Halaman dasbor yang ditandai public akan disimpan CloudFront dan disajikan ke pengguna berikutnya yang membuka URL yang sama — lengkap dengan nama, saldo, dan riwayat pesanan orang lain. Ini bukan skenario teoretis; ia terjadi setiap tahun di aplikasi produksi. Pasang default aman: private, no-store untuk semua rute, lalu longgarkan secara eksplisit hanya pada rute publik.

// Default aman lewat middleware
func CacheDefault(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Cache-Control", "private, no-store")
		next.ServeHTTP(w, r)   // handler publik menimpanya secara sadar
	})
}

stale-while-revalidate: yang mengubah segalanya

Tanpa SWR (s-maxage=60):
  detik 60  → cache kedaluwarsa
  detik 60  → 5.000 permintaan bersamaan semuanya menembus ke origin
             (cache stampede, versi CDN)

Dengan SWR (s-maxage=60, stale-while-revalidate=600):
  detik 60  → CDN menyajikan versi lama SEKETIKA ke semua orang
  detik 60  → SATU permintaan latar menyegarkannya
  detik 61  → semua orang dapat versi baru
             → origin menerima 1 permintaan, bukan 5.000

Dan stale-if-error adalah asuransi pemadaman. Kalau aplikasimu mati, CloudFront tetap menyajikan versi terakhir yang berhasil — pembaca anonim tidak melihat apa pun yang salah selama masa itu. Untuk situs konten, ini mengubah pemadaman total jadi pemadaman yang hanya terasa oleh penulis.

ETag dan 304

func (h *Handler) Artikel(w http.ResponseWriter, r *http.Request) {
	a, err := h.artikel.Ambil(r.Context(), slug)
	if err != nil {
		h.tulisError(w, r, err)
		return
	}

	etag := fmt.Sprintf(`"%x"`, sha256.Sum256([]byte(
		a.Slug+a.Diubah.Format(time.RFC3339))))
	w.Header().Set("ETag", etag)
	w.Header().Set("Cache-Control", "public, max-age=60, s-maxage=300")

	// Klien sudah punya versi ini: balas 304, tanpa body.
	if r.Header.Get("If-None-Match") == etag {
		w.WriteHeader(http.StatusNotModified)
		return
	}
	...
}

304 menghemat bandwidth, bukan pekerjaan server. Kamu tetap harus mengambil data untuk menghitung ETag-nya. Kalau tujuanmu mengurangi beban database, s-maxage yang menyelesaikannya — permintaannya tidak pernah sampai. ETag melengkapi, bukan menggantikan.

Kunci cache CDN

Kunci cache CloudFront = host + path + (query/header/cookie yang DIPILIH)

❌ Meneruskan SEMUA query string:
   /artikel?utm_source=fb&utm_campaign=x  → entri cache terpisah
   /artikel?utm_source=ig                  → entri lain lagi
   → rasio hit hancur; hampir semua permintaan tembus ke origin

✅ Teruskan hanya yang MEMENGARUHI keluaran:
   /artikel?halaman=2 → ya
   ?utm_* , ?fbclid , ?gclid → abaikan

❌ Meneruskan semua cookie:
   setiap pengunjung punya cookie analitik unik → 0% hit rate

✅ Teruskan hanya cookie sesi, DAN hanya pada rute yang membutuhkannya

Rasio hit CDN di bawah 90% pada situs konten hampir selalu berarti kunci cache-nya salah — biasanya karena query pelacakan atau cookie ikut masuk kunci. Ini pemeriksaan pertama saat trafik naik dan servermu ikut naik padahal seharusnya tidak.

Header Vary

// Konten berbeda per bahasa atau per encoding
w.Header().Set("Vary", "Accept-Encoding, Accept-Language")

// ❌ Vary: Cookie pada halaman publik = satu entri cache per pengunjung
// ❌ Vary: User-Agent = ribuan entri untuk konten yang sama

Urutan pemeriksaan saat trafik naik

  1. Rasio hit CDN. Di bawah 90% untuk situs konten berarti kunci cache bermasalah.
  2. Kueri per permintaan. Naik seiring jumlah baris = N+1 (Fase 4).
  3. p95 database. Kueri teratas biasanya menunjuk index yang hilang.
  4. Rasio hit cache aplikasi.
  5. Antrean pool database (WaitCount).
  6. Baru tambah task, dan baru pikirkan arsitektur.

Satu hal yang tidak akan diselesaikan infrastruktur: halaman yang butuh empat puluh kueri dan mengirim dua megabita HTML tetap mahal di runtime mana pun, di kelas instans apa pun, di belakang CDN mana pun. Ukur dulu, baru belanja.

Latihan: pasang Cache-Control: public, s-maxage=60, stale-while-revalidate=600 pada satu halaman publik dan verifikasi header X-Cache: Hit from cloudfront pada permintaan kedua. Lalu tambahkan ?utm_source=x dan lihat apakah ia menghasilkan miss — kalau ya, perbaiki kebijakan kunci cache-mu.

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