Cache API aman di edge bukan sekadar menambahkan Cache-Control lalu berharap latensi turun. Begitu respons disimpan di edge, Anda harus memastikan bahwa data publik memang aman dibagikan, data privat tidak bocor antar pengguna, dan request yang diulang oleh client, proxy, atau sistem webhook tetap idempoten.

Masalah utamanya hampir selalu sama: cache key terlalu umum, Authorization diabaikan, cookie membuat respons personal ikut tercache, atau retry webhook menulis data dua kali karena lapisan cache dan origin tidak punya kontrak yang jelas. Solusinya adalah mendesain API dengan aturan eksplisit tentang apa yang boleh di-cache, untuk siapa, dan dengan kunci apa.

Kapan respons API boleh di-cache di edge?

Aturan pertama: jangan mulai dari “semua bisa di-cache”. Mulailah dari klasifikasi respons.

1. Data publik

Respons publik adalah data yang sama untuk semua pengguna, misalnya:

  • daftar produk publik,
  • halaman katalog,
  • metadata konten publik,
  • konfigurasi frontend non-rahasia.

Jenis ini biasanya aman disimpan di edge dengan public cache policy. Anda masih perlu mengontrol masa berlaku dan validasi ulang dengan ETag.

2. Data privat per pengguna

Respons privat adalah data yang tergantung identitas atau hak akses, misalnya:

  • profil pengguna,
  • riwayat transaksi,
  • dashboard internal,
  • fitur yang hasilnya berbeda berdasarkan role atau tenant.

Secara default, respons ini jangan di-cache bersama di edge kecuali Anda benar-benar membuat cache key yang terisolasi per pengguna atau per identitas akses. Dalam banyak kasus, lebih aman memakai Cache-Control: private atau tidak meng-cache sama sekali di shared cache.

3. Respons campuran

Kasus paling berbahaya adalah respons yang terlihat publik tetapi diam-diam personal, misalnya endpoint /articles yang menambahkan flag isBookmarked untuk pengguna login. Jika respons seperti ini di-cache sebagai publik, pengguna lain bisa menerima versi yang salah, atau lebih buruk, data personal ikut terbawa.

Jika satu endpoint memiliki perilaku berbeda untuk pengguna anonim dan pengguna terautentikasi, anggap endpoint tersebut berisiko tinggi sampai kontrak cache-nya benar-benar eksplisit.

Kontrak header yang perlu benar: Cache-Control, ETag, Vary, dan stale-while-revalidate

Cache-Control untuk data publik vs privat

Header Cache-Control menentukan apakah shared cache seperti edge boleh menyimpan respons.

Contoh aman untuk data publik:

Cache-Control: public, max-age=60, stale-while-revalidate=300
ETag: "articles-v42"

Artinya:

  • public: boleh disimpan di shared cache,
  • max-age=60: selama 60 detik respons dianggap segar,
  • stale-while-revalidate=300: edge boleh menyajikan versi lama sambil menyegarkan ke origin,
  • ETag: origin dan edge bisa melakukan validasi ulang hemat bandwidth.

Contoh aman untuk data privat:

Cache-Control: private, no-cache
ETag: "user-123-profile-v8"

Di sini private memberi sinyal bahwa respons ditujukan untuk satu pengguna, bukan untuk shared cache. no-cache bukan berarti tidak boleh disimpan sama sekali; artinya respons harus divalidasi ulang sebelum dipakai kembali. Untuk data sensitif, banyak tim memilih no-store jika ingin memastikan tidak ada penyimpanan di lapisan perantara.

Peran ETag: validasi ulang tanpa menebak-nebak

ETag adalah pengenal versi representasi resource. Saat client atau edge punya salinan lama, ia bisa mengirim:

If-None-Match: "articles-v42"

Jika konten belum berubah, origin cukup menjawab 304 Not Modified tanpa body. Ini penting untuk edge caching karena:

  • mengurangi transfer data,
  • menghindari regenerasi payload besar,
  • membuat revalidasi lebih aman dibanding TTL panjang tanpa validasi.

Jangan membuat ETag yang sama untuk representasi yang berbeda. Jika respons dipengaruhi locale, tenant, atau bentuk field berdasarkan permission, maka ETag juga harus merepresentasikan variasi itu atau Anda harus memisahkannya lewat cache key dan Vary.

Vary: memberi tahu bahwa satu URL punya beberapa representasi

Vary penting ketika respons berubah berdasarkan header request tertentu. Contoh umum:

Vary: Accept-Encoding, Accept-Language

Untuk API, Vary bisa relevan jika:

  • bahasa memengaruhi hasil,
  • format respons bergantung pada header tertentu,
  • perilaku anonim vs login dibedakan oleh header yang memang ingin dijadikan bagian dari cache key.

Namun ada jebakan besar: menambahkan Vary: Authorization tidak otomatis berarti semua edge cache akan aman dan efisien. Secara teori ini memberi tahu bahwa respons bervariasi berdasarkan header Authorization, tetapi pada praktiknya:

  • banyak token unik per request atau per sesi, sehingga hit ratio hampir nol,
  • beberapa platform cache tidak menangani shared caching atas respons ber-Authorization seperti yang Anda bayangkan,
  • jika token membawa konteks sensitif, menjadikannya bagian dari cache key bisa meningkatkan kompleksitas dan risiko observability/logging.

Kesimpulannya: jangan mengandalkan Vary: Authorization sebagai solusi utama untuk data privat. Lebih baik pisahkan endpoint publik dan privat, atau gunakan cache key yang diturunkan dari identitas stabil dan aman bila Anda benar-benar memerlukan edge caching untuk data privat.

stale-while-revalidate: cepat, tapi tetap butuh batasan

stale-while-revalidate berguna untuk mengurangi latensi puncak dan mencegah herd effect saat cache kedaluwarsa. Tetapi ada dua aturan:

  1. Gunakan terutama untuk data publik atau data yang aman disajikan sedikit lama.
  2. Jangan memakainya untuk respons yang sensitif terhadap perubahan otorisasi, status pembayaran, atau entitlement pengguna, kecuali Anda menerima risiko data sedikit basi.

Contoh buruk: status langganan premium di-cache 5 menit dengan stale-while-revalidate. Pengguna yang baru dicabut aksesnya bisa tetap melihat versi lama untuk sementara.

Jebakan umum pada Authorization, cookie, dan cache key

Anti-pattern: URL sama, respons berbeda, cache key sama

Misalnya backend punya endpoint:

GET /me/orders

Lalu edge cache memakai key berbasis URL saja. Ini langsung berbahaya karena pengguna A dan B mengakses URL yang sama tetapi seharusnya menerima data berbeda.

Jika endpoint bersifat personal, pilihan aman adalah:

  • tidak menyimpannya di shared cache, atau
  • membangun cache key yang mengandung identitas pengguna/tenant yang sudah dinormalisasi dan aman.

Authorization header sering membuat asumsi salah

Banyak tim mengira “kalau ada Authorization, cache pasti tidak akan menyimpan”. Asumsi ini tidak universal. Perilaku tergantung platform, konfigurasi, dan kode edge Anda sendiri. Jika Anda menulis logika cache manual di edge, Anda bisa saja tanpa sengaja menyimpan respons privat walaupun request membawa bearer token.

Prinsip yang aman:

  • Jika request membawa Authorization, anggap respons tidak boleh masuk shared cache kecuali ada desain eksplisit sebaliknya.
  • Jika sebagian data tetap publik walau user login, pertimbangkan endpoint terpisah untuk versi publik.
  • Hindari membuat cache key langsung dari token mentah.

Cookie-based auth lebih mudah bocor daripada yang terlihat

Autentikasi berbasis cookie sering lebih sulit dideteksi di layer edge, terutama jika endpoint publik dan privat berbagi domain/path yang sama. Risiko umumnya:

  • cookie sesi ikut terkirim ke endpoint yang tampak publik,
  • backend menambah field personal karena mendeteksi cookie,
  • edge menyimpan respons yang ternyata sudah terpersonalisasi.

Mitigasi praktis:

  • untuk endpoint yang ingin benar-benar publik, pastikan origin mengabaikan cookie autentikasi,
  • gunakan path atau host terpisah untuk API publik dan privat bila memungkinkan,
  • jangan menambahkan personalisasi implisit pada endpoint yang direncanakan untuk shared cache.

Cache key harus berbasis identitas representasi, bukan mentah dari request

Cache key yang baik merepresentasikan hal-hal yang benar-benar mengubah body respons. Biasanya ini mencakup:

  • metode request,
  • path,
  • query yang relevan,
  • locale atau format respons,
  • tenant atau identitas stabil jika memang meng-cache data privat secara terisolasi.

Jangan memasukkan semua header secara membabi buta. Jangan juga memakai token JWT mentah sebagai key karena:

  • ukuran key membengkak,
  • hit ratio rendah,
  • token bisa berganti walau identitas sama,
  • risiko token terekspos lewat log atau tooling.

Jika harus meng-cache respons per pengguna di edge, lebih aman turunkan key dari identitas stabil seperti tenant_id:user_id:resource_version, bukan dari bearer token mentah.

Skenario backend yang aman dan yang berbahaya

Skenario aman: katalog publik

Endpoint:

GET /v1/products?category=books&lang=id

Karakteristik:

  • sama untuk semua pengguna,
  • berubah tidak terlalu sering,
  • bahasa memengaruhi representasi.

Kontrak yang masuk akal:

Cache-Control: public, max-age=120, stale-while-revalidate=600
ETag: "products-books-id-v154"
Vary: Accept-Language

Atau jika bahasa ditentukan eksplisit lewat query, Anda mungkin tidak butuh Vary: Accept-Language karena query sudah menjadi bagian dari key.

Skenario berbahaya: endpoint publik yang diam-diam personal

Endpoint:

GET /v1/products/123

Untuk pengguna anonim, respons hanya berisi detail produk. Untuk pengguna login, origin menambah:

  • is_in_wishlist,
  • last_viewed_price,
  • promo spesifik akun.

Jika edge menyimpan salah satu versi itu sebagai respons publik, hasilnya salah atau bocor. Solusi yang lebih aman:

  • pisahkan endpoint publik dan state personal, misalnya /v1/products/123 dan /v1/me/product-flags/123, atau
  • jangan cache respons gabungan di shared cache.

Skenario aman terbatas: caching privat terisolasi per tenant

Ada kasus internal B2B di mana dashboard dibaca sangat sering dan data per tenant cukup stabil. Secara teori edge caching tetap bisa dipakai jika:

  • cache key memuat tenant yang tervalidasi,
  • respons tidak bercampur antar tenant,
  • TTL pendek,
  • invalidation jelas,
  • audit logging memadai.

Tetap saja, kompleksitasnya jauh lebih tinggi dibanding caching data publik. Banyak sistem lebih aman menggunakan cache di origin atau aplikasi daripada shared edge cache untuk data seperti ini.

Idempotensi saat webhook, retry, dan cache terlibat

Edge cache umumnya relevan untuk request GET dan kadang HEAD. Tetapi dalam sistem nyata, lapisan cache bisa tetap memengaruhi alur retry, validasi status, atau pembacaan setelah penulisan. Karena itu idempotensi tetap harus dirancang di tingkat API dan backend.

Kenapa webhook dan retry sering rusak

Masalah umum:

  • provider webhook mengirim event yang sama lebih dari sekali,
  • client mengulang request karena timeout,
  • load balancer atau worker menjalankan retry internal,
  • response sukses lambat sampai ke client, sehingga client mengira gagal lalu mengirim ulang.

Jika endpoint write seperti POST /payments/webhook tidak idempoten, satu event bisa membuat dua insert, dua email, atau dua perubahan status.

Aturan penting: jangan cache endpoint write

Untuk endpoint webhook, callback, atau mutasi data:

Cache-Control: no-store

Lebih penting lagi, logika handler harus idempoten secara bisnis. Cache tidak boleh menjadi mekanisme pencegah duplikasi write.

Pola idempotency key

Untuk endpoint yang bisa di-retry client, gunakan idempotency key yang stabil per operasi. Backend menyimpan hasil pemrosesan berdasarkan key tersebut.

POST /v1/orders
Idempotency-Key: 8b6c2d3e-...

Pseudocode handler:

if idempotency_key exists and already_processed:
    return previous_response

begin transaction
  check business uniqueness
  create order
  store idempotency result
commit

return created_response

Kenapa ini penting walau ada cache:

  • retry bisa terjadi sebelum cache terisi,
  • write endpoint seharusnya tidak lewat shared cache,
  • duplikasi harus dicegah di sumber kebenaran, yaitu backend dan database.

Interaksi read-after-write dengan edge cache

Setelah operasi write berhasil, client sering memanggil endpoint GET yang mungkin masih menyajikan data lama dari edge. Ini bukan bug auth atau bug bisnis, melainkan konsekuensi konsistensi cache.

Mitigasi:

  • TTL pendek untuk resource yang sering berubah,
  • revalidasi dengan ETag,
  • purge/invalidation ketika write penting terjadi, jika platform mendukung,
  • respons write mengembalikan representasi terbaru sehingga client tidak perlu langsung melakukan GET.

Contoh desain kontrak API

1. Endpoint publik yang aman di-cache

GET /v1/articles/redis-at-scale

200 OK
Cache-Control: public, max-age=60, stale-while-revalidate=300
ETag: "article-redis-at-scale-v17"
Content-Type: application/json

{
  "slug": "redis-at-scale",
  "title": "Redis at Scale",
  "body": "..."
}

Karena body sama untuk semua pengguna, edge cache aman. Jika artikel diubah, versi ETag ikut berubah atau resource dipurge.

2. Endpoint profil pengguna yang tidak boleh dibagi

GET /v1/me
Authorization: Bearer <token>

200 OK
Cache-Control: private, no-cache
ETag: "user-42-profile-v3"
Content-Type: application/json

{
  "id": 42,
  "email": "[email protected]",
  "plan": "pro"
}

Respons ini boleh divalidasi ulang oleh client tertentu, tetapi tidak semestinya dibagikan oleh shared edge cache.

3. Memisahkan data publik dan state personal

GET /v1/products/123
Cache-Control: public, max-age=120, stale-while-revalidate=600
ETag: "product-123-v55"

GET /v1/me/product-flags/123
Authorization: Bearer <token>
Cache-Control: private, no-store

Pemisahan ini biasanya jauh lebih aman dan lebih mudah di-debug dibanding menggabungkan semua field dalam satu endpoint.

Checklist implementasi cache API aman di edge

  1. Klasifikasikan endpoint: publik, privat, atau campuran.
  2. Tetapkan default aman: request dengan auth atau cookie sesi tidak masuk shared cache kecuali ada pengecualian eksplisit.
  3. Definisikan cache key hanya dari komponen yang benar-benar memengaruhi representasi.
  4. Gunakan Cache-Control yang sesuai: public untuk data publik, private atau no-store untuk data sensitif.
  5. Gunakan ETag untuk revalidasi, terutama pada resource yang sering dibaca.
  6. Gunakan Vary secara disiplin, bukan sebagai penutup desain yang kabur.
  7. Hindari personalisasi implisit pada endpoint yang akan di-cache bersama.
  8. Jangan cache endpoint write, webhook, callback, atau operasi yang memicu efek samping.
  9. Terapkan idempotency key untuk request mutasi yang mungkin diulang.
  10. Rencanakan invalidation untuk data yang berubah karena aksi admin, publish konten, atau update harga.

Anti-pattern yang sering muncul

  • Meng-cache semua GET tanpa memeriksa apakah body dipengaruhi auth atau cookie.
  • Menggunakan token mentah sebagai cache key.
  • Mencampur data publik dan personal dalam satu payload lalu menandainya public.
  • Mengandalkan TTL panjang tanpa ETag atau invalidation.
  • Menganggap no-cache sama dengan no-store.
  • Menganggap retry tidak masalah karena “client jarang mengulang”.
  • Menganggap bug duplikasi write adalah masalah cache, padahal akar masalahnya tidak idempoten.

Observability: membedakan bug auth, bug cache, dan duplikasi request

Tanpa observability yang tepat, tiga jenis bug ini terlihat mirip: pengguna menerima data yang salah, data terasa basi, atau resource tercipta dua kali. Anda perlu metadata yang cukup untuk membedakannya.

Apa yang sebaiknya dicatat

  • request id unik per request,
  • correlation id lintas edge, origin, dan worker internal,
  • cache status: hit, miss, revalidated, bypass, stale,
  • cache key fingerprint yang aman, bukan key mentah jika mengandung data sensitif,
  • auth context minimal: anonymous, user_id hash, tenant_id, role ringkas,
  • idempotency key untuk endpoint write,
  • ETag request/response saat troubleshooting revalidasi.

Cara membaca gejalanya

Gejala bug auth:

  • request masuk ke user atau tenant yang salah,
  • origin mengembalikan body personal padahal seharusnya anonim,
  • cache status mungkin hanya memperparah, tetapi akar masalah ada pada identitas/otorisasi.

Gejala bug cache:

  • body benar untuk satu konteks tetapi disajikan ke konteks lain,
  • cache hit terjadi pada endpoint yang semestinya bypass/private,
  • versi lama tetap muncul karena invalidation gagal atau key terlalu umum.

Gejala duplikasi request:

  • dua atau lebih request dengan payload sama dalam jendela waktu dekat,
  • request id berbeda tetapi idempotency key sama,
  • efek samping tercipta dua kali karena backend tidak mengunci operasi secara idempoten.

Debugging praktis

  1. Ambil satu request bermasalah dengan request id.
  2. Periksa apakah request membawa Authorization atau cookie autentikasi.
  3. Lihat cache status di edge dan origin.
  4. Bandingkan cache key fingerprint untuk request yang seharusnya berbeda.
  5. Periksa apakah origin mengirim Cache-Control, ETag, dan Vary sesuai kontrak.
  6. Untuk operasi write, cari idempotency key dan cek apakah operasi sebelumnya sudah diproses.

Penutup

Cache API aman di edge bergantung pada kontrak yang jelas, bukan pada optimasi belakangan. Untuk data publik, gunakan Cache-Control, ETag, dan bila perlu stale-while-revalidate agar cepat sekaligus efisien. Untuk data privat, default yang aman adalah tidak menyimpannya di shared cache kecuali Anda punya cache key terisolasi dan observability yang kuat.

Jika satu hal perlu diingat: pisahkan data publik dari data personal sedini mungkin. Langkah ini menyederhanakan cache key, mencegah kebocoran auth, memudahkan invalidation, dan membuat bug duplikasi request lebih mudah dibedakan dari bug cache.