← semua materi
15:00 Teknis Β· 23 Oct 2026 Β· 15:12

πŸ“š Topik

CDN & edge caching: cache key, purge, dan staleness

🎯 Tujuan Belajar

Besok bisa:


πŸ—οΈ Arsitektur & Alur Teknis

Client
  β”‚
  β–Ό
CDN Edge
  β”œβ”€ Cache HIT  ──► Response langsung
  └─ Cache MISS
        β”‚
        β–Ό
   Origin Server
        β”‚
        β”œβ”€ 200 + body + cache headers
        β”œβ”€ 304 jika ETag cocok
        └─ 5xx jika gagal

Request cache entry ditentukan oleh cache key.

Contoh cache key:

scheme://host/path?query

CDN dapat menambah:

Cloudflare mendokumentasikan default cache key yang mencakup URL penuh, Origin, beberapa method-override headers, dan forwarding headers [1].

Cache key terlalu sempit

/api/profile

Semua user bisa menerima response user pertama. Risiko: data leak.

Cache key terlalu lebar

/api/catalog?utm_source=x&session_id=abc&timestamp=...

Setiap request membuat objek cache baru. Hit ratio turun.

Cache key sehat

/catalog?page=2&sort=price

Tracking parameter seperti utm_source tidak ikut jika tidak memengaruhi response.


Model TTL

Tiga waktu perlu dibedakan:

fresh TTL
  β”‚
  β”œβ”€ stale-while-revalidate window
  β”‚
  └─ stale-if-error window

Contoh:

Cache-Control: public, max-age=60, s-maxage=300, \
stale-while-revalidate=30, stale-if-error=3600

Makna:

stale-while-revalidate dan stale-if-error didefinisikan RFC 5861 [2].

max-age vs s-maxage

Directive Target Contoh
max-age Browser dan private cache 60 detik
s-maxage Shared cache seperti CDN 300 detik
stale-while-revalidate Grace period saat refresh 30 detik
stale-if-error Fallback saat origin gagal 3600 detik
immutable Asset URL fingerprinted 1 tahun

πŸ’» Kode Nyata / Konfigurasi

1. Response header berdasarkan tipe konten

# HTML publik, boleh sedikit stale
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30, stale-if-error=3600
ETag: "article-42-v7"
Vary: Accept-Encoding

# JavaScript/CSS dengan content hash
Cache-Control: public, max-age=31536000, immutable

# Data personalized
Cache-Control: private, no-store

# API publik dengan toleransi staleness
Cache-Control: public, max-age=0, s-maxage=10, stale-while-revalidate=30, stale-if-error=300
ETag: "catalog-v19"

Jangan cache response authenticated hanya karena response berstatus 200.

Authorization: Bearer ...
Cache-Control: private, no-store

2. Origin Python minimal yang bisa dijalankan

Python 3.11+:

#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from hashlib import sha256
from email.utils import formatdate
import json
import time

PORT = 8080
VERSION = "catalog-v19"

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/catalog":
            self.send_error(404)
            return

        body = json.dumps(
            {"items": [{"id": 1, "name": "Keyboard"}], "version": VERSION},
            separators=(",", ":"),
        ).encode()

        etag = '"' + sha256(body).hexdigest()[:16] + '"'

        if self.headers.get("If-None-Match") == etag:
            self.send_response(304)
            self.send_header("ETag", etag)
            self.end_headers()
            return

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header(
            "Cache-Control",
            "public, max-age=0, s-maxage=10, "
            "stale-while-revalidate=30, stale-if-error=300",
        )
        self.send_header("ETag", etag)
        self.send_header("Date", formatdate(time.time(), usegmt=True))
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()

Jalankan:

python3 origin.py

Uji response:

curl -i http://127.0.0.1:8080/catalog

Uji conditional request:

etag="$(curl -sD - http://127.0.0.1:8080/catalog -o /dev/null \
  | tr -d '\r' | awk '$1 == "ETag:" {print $2}')"

curl -i -H "If-None-Match: $etag" http://127.0.0.1:8080/catalog

Expected status kedua:

HTTP/1.0 304 Not Modified

304 menghemat body transfer. CDN tetap dapat memperpanjang lifetime object setelah validasi.

3. Cache key dengan allowlist query

from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

CACHE_QUERY = frozenset({"page", "sort", "filter"})

def cache_key(url: str) -> str:
    parts = urlsplit(url)
    query = urlencode(
        sorted(
            (key, value)
            for key, value in parse_qsl(parts.query, keep_blank_values=True)
            if key in CACHE_QUERY
        )
    )
    return urlunsplit((parts.scheme, parts.netloc, parts.path, query, ""))

assert cache_key(
    "https://example.test/catalog?utm_source=x&page=2"
) == "https://example.test/catalog?page=2"

assert cache_key(
    "https://example.test/catalog?page=2&sort=price"
) == "https://example.test/catalog?page=2&sort=price"

print("cache key checks passed")

Jangan menghapus query parameter sebelum membuktikan parameter itu tidak mengubah response.

4. Vary

Jika bahasa memengaruhi response:

HTTP/1.1 200 OK
Cache-Control: public, s-maxage=600
Vary: Accept-Language
Content-Type: text/html

Jika device atau user ID memengaruhi response, jangan otomatis menambah header itu ke cache key. High-cardinality header dapat membuat hampir setiap request menjadi cache entry berbeda. Vercel secara eksplisit memperingatkan masalah ini [6].

Cloudflare mendukung normalisasi Accept, Accept-Language, dan Accept-Encoding untuk mengurangi cache fragmentation [3].

5. Cache tags untuk invalidasi grup

Origin:

HTTP/1.1 200 OK
Cache-Control: public, s-maxage=600
Cache-Tag: product-42,products

Saat produk berubah:

curl -X POST \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"tags":["product-42"]}'

Cloudflare mendukung sampai 100 tag per operasi purge berdasarkan dokumentasi yang diperbarui 21 Agustus 2026 [4].

Fastly memakai konsep serupa bernama Surrogate-Key dan mendukung soft purge [7].

6. Invalidate vs purge

Operasi Object Request berikutnya Cocok untuk
Purge Dihapus Fetch penuh dari origin Data salah atau sensitif
Invalidate Tetap ada, ditandai stale Conditional revalidation Update normal
TTL expiry Menjadi stale alami Bergantung konfigurasi Konten toleran delay

Cloudflare menjelaskan bahwa invalidation dapat memakai ETag atau Last-Modified; response 304 mempertahankan body cache [5].


πŸ“Š Angka & Benchmark

Formula dasar

cache hit ratio = cache hits / total requests Γ— 100%
origin requests = total requests Γ— (1 - hit ratio)

Traffic:

100 juta request/bulan
Hit ratio Request ke origin
60% 40 juta
85% 15 juta
95% 5 juta

Tidak ada angka hit ratio universal. Static fingerprinted assets bisa mendekati hit ratio tinggi. Personalized API sering tidak layak di-cache.

Sumber industri melaporkan contoh hit ratio media di atas 90%, tetapi angka itu bukan benchmark universal dan bukan jaminan untuk aplikasi Hilmi [8]. Ukur trafik sendiri berdasarkan:


βš–οΈ Trade-off & Alternatif

Long TTL + purge

s-maxage=86400

Kelebihan:

Kekurangan:

Cocok untuk:

Short TTL + revalidation

s-maxage=10, stale-while-revalidate=30

Kelebihan:

Kekurangan:

Cocok untuk:

Versioned URL

/app.8f31c2.js

Kelebihan:

Kekurangan:

Cocok untuk:

CDN stale fallback

Kelebihan:

Kekurangan:


🏭 Use Case Produksi & Failure Mode Umum

1. Cache poisoning

Penyebab:

Mitigasi:

2. Web cache deception

Contoh pola berbahaya:

/account/profile.css

Origin menganggap path sebagai halaman private. CDN menganggap suffix .css sebagai static asset dan menyimpan response.

Cloudflare dan PortSwigger mendokumentasikan risiko perbedaan parsing path antara cache dan origin [9][10].

Mitigasi:

/private/*  β†’ no-store
/api/*      β†’ no-store kecuali allowlist eksplisit

Jangan menentukan cacheability hanya berdasarkan ekstensi file.

3. Cache stampede

Banyak edge request melihat object expired bersamaan lalu semuanya memanggil origin.

Mitigasi:

4. Purge sebelum origin update

Urutan salah:

purge CDN
update origin

Window kosong menyebabkan request pertama meng-cache data lama atau error.

Urutan aman:

update origin
verify origin
purge/invalidate CDN
verify edge

5. RSC cache-key mismatch

Next.js memakai request variant untuk HTML dan React Server Components. Dokumentasi Next.js menyebut _rsc harus dipertahankan dalam cache key untuk static prefetch, bersama header next-router-prefetch pada alur prefetch [11].

Jangan membuat CDN rule yang membuang semua query parameter pada route Next.js tanpa memahami _rsc.

6. Deployment race

HTML baru menunjuk asset baru, tetapi edge berbeda masih menyajikan HTML lama atau RSC payload lama.

Mitigasi:


πŸ› οΈ Latihan Praktis Kecil

Latihan A β€” Ukur cache behavior

Ambil endpoint publik milik sendiri:

URL='https://example.com/'

for i in 1 2 3; do
  curl -sS -D - "$URL" -o /dev/null \
    | grep -Ei '^(HTTP/|age:|cache-control:|etag:|vary:|x-cache:|cf-cache-status:|x-vercel-cache:)'
  sleep 2
done

Catat:

Latihan B β€” Test cache key fragmentation

Kirim URL sama dengan tracking parameter:

for query in \
  'page=2' \
  'page=2&utm_source=x' \
  'page=2&utm_source=y' \
  'page=2&sort=price'; do
  curl -sS -o /dev/null -w "$query %{http_code} %{time_total}\n" \
    "https://example.com/catalog?$query"
done

Jika utm_source membuat cache object terpisah, hapus hanya setelah memastikan tracking tidak memengaruhi response.

Latihan C β€” Simulasikan conditional request

python3 origin.py

Terminal lain:

curl -i http://127.0.0.1:8080/catalog

Salin nilai ETag, lalu:

curl -i \
  -H 'If-None-Match: "PASTE_ETAG"' \
  http://127.0.0.1:8080/catalog

Target: status 304 Not Modified.


βœ… Cek Pemahaman

  1. Mengapa Vary: Authorization biasanya lebih berbahaya daripada berguna?
  2. Kapan memilih invalidate dibanding purge?
  3. Mengapa asset fingerprinted dapat memakai immutable, tetapi HTML biasanya tidak?

πŸ”‘ Poin Kunci

Status pengetahuan


➑️ Eksplorasi Lanjut

πŸ”— Sumber

[1] Cloudflare. β€œCache Keys.” Dokumentasi diperbarui 22 Oktober 2026.
https://developers.cloudflare.com/cache/how-to/cache-keys/

[2] M. Nottingham. β€œRFC 5861: HTTP Cache-Control Extensions for Stale Content.” Mei 2010.
https://www.rfc-editor.org/info/rfc5861

[3] Cloudflare. β€œVary.” Dokumentasi Cache.
https://developers.cloudflare.com/cache/concepts/vary/

[4] Cloudflare. β€œPurge cache by cache-tags.” Dokumentasi diperbarui 21 Agustus 2026.
https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/

[5] Cloudflare. β€œInvalidate cached content.” Dokumentasi diperbarui 29 September 2026.
https://developers.cloudflare.com/cache/guides/invalidate-cache/

[6] Vercel. β€œVercel CDN Cache.”
https://vercel.com/docs/caching/cdn-cache

[7] Fastly. β€œSoft purges.”
https://www.fastly.com/documentation/guides/full-site-delivery/purging/soft-purges/

[8] BlazingCDN. β€œHow a CDN Reduces Server Load and Bandwidth Costs.” Angka bersifat contoh industri, bukan benchmark universal.
https://blog.blazingcdn.com/en-us/how-to-use-a-cdn-to-reduce-server-load-and-bandwidth-costs

[9] PortSwigger. β€œWeb cache deception.”
https://portswigger.net/web-security/web-cache-deception

[10] Cloudflare. β€œCache Deception Armor.”
https://developers.cloudflare.com/cache/cache-security/cache-deception-armor/

[11] Next.js. β€œUsing a CDN with Next.js.”
https://nextjs.org/docs/app/guides/cdn-caching