Stale-While-Revalidate Mengeluarkan Refresh Cache dari Request Path

Cache entry tidak langsung kehilangan seluruh kegunaannya tepat saat freshness timer habis. Untuk sebagian data, value yang berumur beberapa detik tetap lebih berguna daripada membuat setiap caller menunggu backend refresh. Stale-while-revalidate memakai toleransi tersebut secara eksplisit: cache dapat menyajikan value yang sudah expired selama interval terbatas sementara refresh berjalan terpisah.

Policy ini mengubah refresh dari kewajiban di request path menjadi background work untuk entry yang masih dapat diterima saat stale. Latency spike di sekitar expiration dapat berkurang, tetapi service harus memiliki batas jelas mengenai umur maksimum response yang masih boleh disajikan.

Fresh dan stale memakai time window terpisah

Entry dapat membawa dua boundary, bukan hanya satu:

created_at
   |
   +---- fresh window ----+
                          |
                          +---- stale-allowed window ----+
                                                        |
                                                        +--> tidak dapat dipakai

Selama fresh window, cache mengembalikan value tanpa refresh. Setelah freshness habis, value masih dapat dikembalikan selama stale allowance belum berakhir. Caller atau background worker memulai revalidation agar request berikutnya dapat memperoleh replacement yang fresh.

Setelah stale allowance habis, cache tidak lagi menganggap value lama dapat diterima. Request harus mengikuti miss policy biasa: menunggu load, gagal, atau memakai fallback lain yang memang ditetapkan service.

Boundary kedua ini merupakan kontrol keselamatan utama. Tanpanya, kegagalan background refresh dapat mengubah stale response sementara menjadi penggunaan data usang tanpa batas.

Satu refresh sebaiknya mewakili satu cache key

Expiration dapat menarik banyak request pada waktu yang sama. Jika setiap stale hit memulai refresh sendiri, cache memang melindungi latency caller tetapi tetap mengirim burst duplicate work ke backend.

Koordinasi refresh karena itu perlu memakai cached object sebagai key. Stale hit pertama dapat mendaftarkan in-flight refresh; stale hit berikutnya tetap menyajikan value lama tanpa menjalankan pekerjaan ekuivalen.

request A -- stale hit --+
request B -- stale hit --+--> satu refresh --> ganti entry
request C -- stale hit --+

Masalah concurrency ini juga muncul pada cold cache miss, tetapi caller tidak perlu menunggu shared load selama stale value masih diizinkan.

In-flight marker perlu dibersihkan setelah berhasil maupun gagal. Refresh yang gagal tidak boleh membuat key selamanya berstatus sedang di-refresh.

Kegagalan refresh harus mempertahankan batas umur

Background refresh dapat timeout, mengembalikan error, atau menghasilkan response yang gagal validation. Mempertahankan entry lama untuk attempt berikutnya dapat masuk akal, tetapi umur aslinya tetap penting.

Mengatur ulang freshness timestamp setelah refresh gagal akan membuat data lama tampak baru. Cache perlu mempertahankan metadata yang menunjukkan kapan representation yang disajikan benar-benar diperoleh atau divalidasi.

Request berikutnya dapat memicu refresh lain sesuai retry policy yang terbatas. Backoff dan jitter berguna ketika backend sedang tidak sehat; tanpa kontrol itu, setiap stale key dapat melakukan retry agresif dan menambah tekanan saat insiden.

Ketika stale allowance berakhir, sistem mengikuti hard-expiry behavior yang sudah dikonfigurasi. Menyajikan data melewati titik tersebut merupakan availability policy berbeda dan tidak seharusnya terjadi secara tidak sengaja.

Stale window bergantung pada semantics data

Product description, feature configuration, account balance, authorization decision, dan one-time credential tidak memiliki toleransi staleness yang sama.

Data yang terkait dengan security atau correctness dapat memerlukan larangan stale serving sepenuhnya. Data lain mungkin mengizinkan umur beberapa detik atau menit karena keterlambatan propagasi kecil tidak mengubah keamanan operasi.

Stale window karena itu sebaiknya menjadi bagian dari cache contract untuk kelas data tertentu, bukan default universal untuk seluruh cache. Policy perlu mempertimbangkan frekuensi mutation, konsekuensi value lama, dan consistency guarantee yang diberikan kepada caller.

Invalidation juga tetap penting. Jika sistem menerima signal andal bahwa cached value sudah tidak valid, stale-while-revalidate tidak seharusnya mengabaikan signal tersebut hanya karena age window masih mengizinkan stale data.

Replacement perlu atomic dari sudut pandang reader

Refresh yang berhasil menghasilkan value baru beserta cache metadata baru. Reader sebaiknya melihat entry lama yang lengkap atau replacement baru yang lengkap, bukan state yang baru diperbarui sebagian.

Pada in-process cache, implementasi dapat mengganti satu immutable entry reference. Pada shared cache, satu atomic set dapat menyediakan boundary yang dibutuhkan. Object yang lebih kompleks mungkin memerlukan versioning atau transactional storage.

Refresh juga perlu terlindung dari completion yang tidak berurutan. Jika dua refresh dapat berjalan akibat race atau worker restart, load yang lebih lama tidak seharusnya menimpa value lebih baru hanya karena selesai belakangan. Version check, generation number, atau compare-and-set semantics dapat mempertahankan ordering ketika race ini relevan.

Background work tetap memakai capacity

Memindahkan refresh dari request path tidak membuat refresh bebas biaya. Database query, remote call, decompression, dan serialization tetap memakai backend resource yang sama.

Refresh worker karena itu memerlukan concurrency limit dan queue bound. Saat banyak entry expired bersamaan, background refresh tanpa batas dapat memenuhi dependency meskipun request latency pada awalnya tampak sehat.

Randomized expiration dapat mengurangi refresh yang tersinkronisasi pada banyak key. Pre-refresh untuk hot key tertentu sebelum hard expiry juga dapat menyebarkan pekerjaan, tetapi tetap perlu dibatasi oleh demand yang teramati dan backend capacity.

Metric sebaiknya menghitung refresh work secara terpisah dari foreground miss agar biayanya terlihat.

Observability memerlukan umur data, bukan hanya hit rate

Cache hit rate tinggi dapat tampak sehat walaupun sebagian besar response stale karena refresh gagal. Hit rate saja tidak dapat membedakan layanan fresh dari stale service yang sedang terdegradasi.

Signal yang berguna mencakup fresh hit, stale hit, hard miss, refresh start, refresh success, refresh failure, refresh duration, umur entry saat disajikan, serta entry yang melewati hard-expiry boundary.

Distribusi umur data yang disajikan sangat penting. Signal tersebut menunjukkan apakah caller biasanya menerima value sedikit melewati freshness atau value yang sudah mendekati stale allowance maksimum.

Alert dapat menggabungkan stale-hit ratio dengan refresh failure dan backend health. Stale ratio yang naik selama insiden backend dapat sesuai ekspektasi; stale ratio yang tetap tinggi setelah recovery dapat menunjukkan refresh coordination macet atau expiry metadata yang keliru.

Bounded staleness adalah contract utama

Stale-while-revalidate bekerja ketika service dapat memisahkan kondisi “tidak fresh” dari “tidak dapat diterima.” Fresh window menentukan reuse normal. Stale window menentukan periode terbatas ketika latency caller dapat diprioritaskan sementara refresh berjalan di tempat lain.

Contract tersebut memerlukan batas eksplisit: satu coordinated refresh per key, umur data yang tetap terjaga saat failure, atomic replacement, background concurrency terbatas, serta hard point setelah value lama tidak lagi disajikan.

Dengan boundary tersebut, stale serving bukan failure mode cache yang kebetulan terjadi. Ia menjadi availability choice yang disengaja dengan maximum age yang dapat diukur.