Cache stampede pada worker dan queue di Redis biasanya muncul saat banyak worker menerima miss pada key yang sama, lalu semuanya ikut menghitung ulang data atau menjalankan job serupa pada waktu hampir bersamaan. Akibatnya, Redis bukan satu-satunya yang sibuk: database, API upstream, service internal, dan sistem queue ikut tertekan karena terjadi duplicate work dalam jumlah besar.
Masalah ini tidak cukup diatasi dengan "menambah TTL" atau "menambah worker". Justru, jika pola akses tidak dikendalikan, jumlah worker yang lebih banyak bisa memperparah stampede. Solusi yang efektif biasanya menggabungkan beberapa teknik: mutex/locking, request coalescing, early recompute, TTL jitter, stale-while-revalidate, idempotensi job, dan observability yang benar.
Gejala operasional yang paling sering muncul
Di sistem nyata, cache stampede jarang terlihat sebagai satu error yang jelas. Gejalanya biasanya tersebar di beberapa metrik dan log.
- Lonjakan traffic ke backend tepat saat key populer habis TTL atau dibersihkan.
- Banyak worker meregenerasi key yang sama dalam rentang waktu sangat pendek.
- Duplicate work pada queue: job dengan parameter identik diproses berulang.
- Latency naik karena worker saling berebut lock, koneksi Redis, atau resource backend.
- Throughput menurun walau jumlah worker tetap atau bahkan dinaikkan.
- Database/API upstream ikut terbebani karena cache miss masif mem-bypass lapisan cache.
- Retry storm jika timeout memicu retry sementara pekerjaan yang sama sebenarnya masih berjalan.
Jika Anda melihat puncak cache miss, peningkatan job identik, dan backend saturation pada waktu yang sama, kemungkinan besar Anda sedang menghadapi cache stampede, bukan sekadar cache miss biasa.
Penyebab utama cache stampede pada worker dan queue Redis
1. TTL key populer habis bersamaan
Ini penyebab paling umum. Banyak item cache dibuat dalam batch dengan TTL yang sama, misalnya 300 detik. Lima menit kemudian, banyak key kadaluarsa hampir serentak. Semua worker yang membutuhkannya langsung jatuh ke jalur regenerasi.
2. Queue memproses job identik secara paralel
Misalnya ada banyak event yang memicu job rebuildUserStats(userId). Tanpa deduplikasi atau idempotensi, worker memproses job yang sebenarnya menghasilkan output yang sama. Redis queue memang cepat, tetapi cepat mengantarkan pekerjaan duplikat tetap berarti boros resource.
3. Tidak ada koordinasi antar worker
Jika setiap worker memakai pola "cek cache, kalau miss langsung hit backend", maka saat miss masif terjadi, semua worker akan melakukan hal yang sama. Tanpa lock atau coalescing, stampede hampir pasti terjadi.
4. Retry yang terlalu agresif
Job yang timeout atau gagal mendapatkan data sering langsung di-retry. Jika penyebab dasarnya adalah cache sedang diregenerasi oleh worker lain, retry justru menambah beban dan membuat antrian makin bising.
5. Invalidasi cache yang terlalu luas
Menghapus banyak key sekaligus setelah deploy, sinkronisasi data, atau rebuild indeks dapat menyebabkan cold cache dalam skala besar. Jika key-key tersebut populer, backend bisa langsung kewalahan.
Alur arsitektur yang lebih aman
Pola yang lebih aman adalah memisahkan tiga hal: pembacaan cache, hak regenerasi, dan penyajian fallback.
- Worker membaca key dari Redis.
- Jika cache masih valid, kembalikan data.
- Jika cache mendekati habis atau sudah habis, worker mencoba mengambil lock untuk regenerasi.
- Hanya satu worker yang boleh meregenerasi key tersebut.
- Worker lain tidak ikut menghitung ulang; mereka menunggu singkat, memakai data stale, atau menjadwalkan cek ulang.
- Setelah data baru tersedia, lock dilepas dan cache diperbarui.
Request/Job Worker
|
v
Read cache in Redis
|
+-- hit & fresh ------------------> return cached value
|
+-- hit but near expiry ----------> serve current value + trigger background refresh
|
+-- miss/expired
|
v
Acquire distributed lock?
|
+-- yes --> regenerate data --> set cache --> release lock
|
+-- no --> wait briefly / use stale value / reschedule
Intinya, sistem harus memastikan bahwa miss pada cache tidak otomatis berarti semua worker boleh regenerasi.
Teknik utama untuk mencegah cache stampede
1. Mutex atau distributed lock
Ini teknik paling langsung: hanya satu worker yang boleh membangun ulang key tertentu. Di Redis, pola umumnya adalah membuat lock dengan operasi atomik seperti set if not exists disertai expiry agar lock tidak menggantung selamanya jika worker mati.
Kapan dipakai:
- Regenerasi data mahal atau lambat.
- Key sangat populer.
- Biaya duplicate work tinggi bagi backend.
Trade-off:
- Jika TTL lock terlalu pendek, lock bisa habis sebelum pekerjaan selesai dan worker lain ikut masuk.
- Jika TTL lock terlalu panjang, sistem lambat pulih saat worker pemegang lock crash.
- Jika pelepasan lock tidak aman, worker lain bisa melepas lock yang bukan miliknya.
Pseudo-code:
function getOrRebuild(key):
value = redis.get(key)
if value exists and not expired:
return value
lockKey = "lock:" + key
lockToken = randomToken()
acquired = redis.set(lockKey, lockToken, NX=true, EX=30)
if acquired:
try:
fresh = rebuildFromBackend(key)
redis.set(key, fresh, EX=300)
return fresh
finally:
releaseLockSafely(lockKey, lockToken)
else:
stale = redis.get("stale:" + key)
if stale exists:
return stale
sleep(100ms)
value = redis.get(key)
if value exists:
return value
throw TemporaryUnavailable
Catatan penting: lock perlu memiliki token unik per pemilik. Saat melepas lock, cocokkan token tersebut agar worker tidak menghapus lock milik worker lain. Ini detail kecil yang sering diabaikan, tetapi penting dalam sistem terdistribusi.
2. Request coalescing
Tujuan request coalescing adalah menyatukan banyak permintaan identik menjadi satu pekerjaan nyata. Jika 100 worker butuh key yang sama, idealnya hanya satu regenerasi yang dilakukan, sedangkan 99 lainnya menunggu hasil yang sama.
Coalescing bisa dilakukan di beberapa titik:
- Di sisi worker: worker yang gagal mendapatkan lock menunggu hasil key yang sama.
- Di sisi queue producer: sebelum enqueue job, cek apakah job identik sudah ada atau sedang diproses.
- Di layer aplikasi: gabungkan permintaan identik dalam memori proses atau melalui coordination key di Redis.
Kapan dipakai: saat sumber stampede berasal dari banyak event identik yang memicu job sama.
Risiko salah konfigurasi: jika kriteria "identik" terlalu longgar, Anda bisa menggabungkan pekerjaan yang sebenarnya berbeda. Jika terlalu ketat, duplikasi tetap lolos.
3. Early recompute
Alih-alih menunggu cache benar-benar habis, sistem dapat meregenerasi data sedikit lebih awal. Misalnya, saat TTL tersisa sangat sedikit, satu worker mulai refresh di background sementara pembaca lain masih menerima data lama yang masih dianggap aman.
Mengapa ini bekerja: Anda memindahkan biaya regenerasi dari momen kritis "setelah expired" ke momen yang lebih tenang "sebelum expired". Ini mengurangi ledakan miss serentak.
Kapan dipakai:
- Key sering diakses.
- Data tidak harus selalu paling baru per detik.
- Regenerasi memakan waktu lebih lama daripada toleransi latency request.
Trade-off: Anda mungkin melakukan refresh lebih sering daripada yang benar-benar perlu. Untuk key jarang diakses, teknik ini bisa memboroskan resource.
4. TTL jitter
TTL jitter berarti menambahkan variasi acak kecil pada TTL agar banyak key tidak habis pada detik yang sama. Ini sederhana, murah, dan sangat berguna jika stampede dipicu oleh batch write dengan TTL identik.
baseTtl = 300
jitter = randomBetween(0, 60)
redis.set(key, value, EX=baseTtl + jitter)Kapan dipakai: hampir selalu layak untuk cache dengan banyak key sejenis.
Trade-off: jitter tidak menyelesaikan duplicate work pada satu key yang sangat populer. Ia hanya mengurangi sinkronisasi kadaluarsa massal.
5. Stale-while-revalidate
Dengan pola stale-while-revalidate, sistem tetap melayani data lama untuk sementara saat refresh sedang berlangsung. Satu worker me-refresh, worker lain menyajikan versi stale yang masih bisa diterima. Ini sangat efektif untuk menahan lonjakan latency dan melindungi backend.
Kapan dipakai:
- Data boleh sedikit terlambat.
- Tujuan utama adalah menjaga ketersediaan dan latency stabil.
- Backend mahal untuk dipanggil berulang.
Tidak cocok jika: data bersifat sangat sensitif terhadap freshness, misalnya saldo real-time, status transaksi final, atau keputusan keamanan.
Risiko: jika jendela stale terlalu panjang, pengguna menerima data lama lebih sering dari yang Anda sadari. Karena itu, observability untuk stale served rate penting.
6. Idempotensi job
Di queue, pencegahan stampede tidak cukup hanya di level cache. Job juga harus idempoten. Artinya, jika job yang sama terproses dua kali karena retry, race condition, atau deduplikasi yang gagal, hasil akhirnya tetap benar dan tidak menimbulkan efek samping ganda.
Contoh:
- Simpan hasil komputasi berdasarkan key deterministik, bukan selalu membuat record baru.
- Gunakan idempotency key untuk operasi yang menulis ke database atau memanggil layanan eksternal.
- Pastikan job dapat mendeteksi bahwa hasil untuk input tertentu sudah tersedia.
Kapan wajib: saat queue memiliki retry, at-least-once delivery, atau worker dapat restart di tengah proses.
Skenario gagal yang sering terjadi
Lock ada, tapi TTL lock terlalu pendek
Satu worker mengambil lock dan mulai menghitung ulang laporan yang butuh 45 detik. TTL lock diatur 10 detik. Setelah 10 detik, lock habis. Worker kedua mengambil lock yang sama dan memulai komputasi ulang. Anda sekarang punya dua regenerasi mahal untuk key yang sama, persis masalah yang ingin dihindari.
Perbaikan: sesuaikan TTL lock dengan durasi kerja yang realistis, beri margin, atau gunakan mekanisme perpanjangan lock jika memang diperlukan dan aman.
Deduplikasi job dilakukan setelah enqueue
Sering ada desain seperti: semua event masuk queue dulu, lalu worker nanti memeriksa apakah kerjaan serupa sudah ada. Ini terlambat. Queue sudah terisi job duplikat dan tetap menimbulkan overhead scheduling, polling, dan retry.
Perbaikan: sedapat mungkin deduplikasi dilakukan sebelum enqueue atau dengan key koordinasi yang dicek atomik.
Semua pembaca menunggu lock tanpa fallback
Saat key expired, satu worker memegang lock. Seratus worker lain menunggu lock sambil busy waiting atau retry rapat. Redis dan CPU justru ikut sibuk karena polling berlebihan.
Perbaikan: gunakan wait singkat dengan backoff, sajikan data stale bila aman, atau reschedule job dengan delay acak kecil.
Invalidasi massal tanpa pemanasan cache
Setelah deploy, ribuan key dihapus untuk memastikan data baru. Lima detik kemudian, traffic normal kembali masuk dan seluruh backend mendadak harus membangun ulang hampir semuanya.
Perbaikan: lakukan invalidasi bertahap, versi key, atau warm-up untuk key populer sebelum menerima beban penuh.
Langkah implementasi bertahap
Tahap 1: Identifikasi key dan job paling berisiko
Jangan mulai dari semua cache. Cari:
- Key dengan QPS tertinggi.
- Regenerasi yang paling mahal.
- Job queue yang paling sering duplikat.
- Endpoint yang memicu lonjakan miss saat TTL habis.
Banyak tim gagal karena mencoba menerapkan locking di semua tempat tanpa prioritas, padahal overhead koordinasi juga punya biaya.
Tahap 2: Tambahkan lock per key untuk jalur regenerasi
Mulailah dari key atau job yang paling mahal. Pastikan:
- Lock dibuat atomik.
- Ada expiry pada lock.
- Pelepasan lock memverifikasi token pemilik.
- Worker lain punya perilaku jelas saat lock gagal: tunggu, pakai stale, atau requeue.
Tahap 3: Tambahkan stale-while-revalidate untuk key populer
Jika user experience tidak menuntut freshness absolut, pola ini biasanya memberi dampak besar pada stabilitas. Simpan data stale untuk jendela pendek, dan catat berapa sering stale disajikan.
Tahap 4: Terapkan TTL jitter
Ini perbaikan cepat yang sering terlupakan. Tambahkan variasi TTL, terutama jika banyak key dibuat oleh batch job atau cron pada waktu yang sama.
Tahap 5: Deduplikasi dan idempotensi di queue
Gunakan key deterministik berdasarkan parameter job. Sebelum enqueue, cek apakah pekerjaan identik sedang tertunda atau sedang berjalan. Di sisi worker, tetap buat job idempoten karena deduplikasi tidak pernah 100% sempurna.
Tahap 6: Tambahkan observability sebelum menyetel threshold lebih jauh
Jangan menebak-nebak apakah solusi berhasil. Tambahkan metrik yang memberi jawaban operasional.
Observability yang wajib dipantau
Tanpa observability, Anda hanya memindahkan masalah. Cache stampede harus terlihat jelas dari metrik, log terstruktur, dan tracing.
Metrik penting
- Cache hit/miss rate per key prefix atau use case.
- Jumlah lock acquired vs lock contention.
- Durasi regenerasi cache.
- Jumlah job identik yang di-enqueue.
- Retry rate dan alasan retry.
- Stale served rate jika memakai stale-while-revalidate.
- Latency backend saat miss meningkat.
- Queue depth, processing lag, dan concurrency efektif.
Log yang membantu debugging
{
"event": "cache_regeneration",
"cache_key": "report:user:123",
"lock_acquired": true,
"worker_id": "worker-7",
"duration_ms": 1840,
"served_stale": false
}Log seperti ini membantu menjawab pertanyaan penting: apakah worker benar-benar saling berebut key yang sama, atau bottleneck justru ada di backend?
Tracing
Jika stack Anda mendukung distributed tracing, hubungkan jalur request -> cache miss -> lock -> backend call -> set cache. Ini memudahkan identifikasi apakah lonjakan latency berasal dari lock contention, Redis, atau service upstream.
Pola pseudo-code yang lebih lengkap untuk worker queue
function processJob(job):
key = buildCacheKey(job.payload)
cached = redis.get(key)
if cached exists and isFresh(cached):
return cached
if cached exists and isWithinStaleWindow(cached):
triggerBackgroundRefreshOnce(key, job.payload)
return cached.value
lockKey = "lock:" + key
token = randomToken()
if redis.set(lockKey, token, NX=true, EX=lockTtl):
try:
existing = redis.get(key)
if existing exists and isFresh(existing):
return existing.value
result = computeExpensiveValue(job.payload)
ttl = baseTtl + randomJitter()
redis.set(key, wrapFreshAndStaleMetadata(result), EX=ttl)
return result
finally:
releaseLockSafely(lockKey, token)
else:
existing = redis.get(key)
if existing exists and isWithinStaleWindow(existing):
return existing.value
requeueWithBackoff(job)
returnAda beberapa detail penting pada pola ini:
- Double-check setelah lock didapat: mungkin worker lain sudah selesai lebih dulu.
- Backoff saat gagal lock: hindari retry rapat.
- Metadata freshness dan stale window: jangan hanya menyimpan value mentah jika Anda butuh kontrol lebih baik.
- Jitter saat set cache: mencegah banyak key habis bersamaan.
Kapan memakai teknik yang mana?
Lock saja sudah cukup jika
- Jumlah key sedikit tapi tiap regenerasi mahal.
- Freshness harus cukup ketat.
- Duplicate work adalah masalah utama.
Tambah stale-while-revalidate jika
- Latency harus stabil saat miss.
- Data boleh sedikit usang.
- Lonjakan traffic sering terjadi pada key populer.
Tambah TTL jitter jika
- Banyak key dibuat bersamaan.
- Stampede muncul dalam pola periodik sesuai TTL.
Tambah deduplikasi queue dan idempotensi jika
- Sumber beban berasal dari event/job identik, bukan hanya request read.
- Anda memiliki retry, replay, atau at-least-once processing.
Tambah early recompute jika
- Anda tahu key tertentu selalu panas.
- Regenerasi mahal tetapi bisa dijadwalkan sebelum expired.
Common mistakes yang mahal di produksi
- Menganggap Redis lock otomatis menyelesaikan semua race condition. Lock hanya satu bagian; queue tetap butuh idempotensi.
- Menggunakan satu lock global untuk banyak key. Ini mengurangi stampede tetapi menurunkan concurrency secara drastis.
- Polling terlalu rapat saat menunggu hasil. Ini memindahkan beban ke Redis.
- Tidak memberi batas waktu pada regenerasi. Backend lambat dapat membuat worker menggantung dan lock menjadi masalah baru.
- Menghapus cache massal saat jam sibuk tanpa warm-up atau rolling invalidation.
- Tidak membedakan stale yang aman dan stale yang berbahaya. Tidak semua data cocok untuk stale-while-revalidate.
Checklist produksi
- Identifikasi key populer dan job mahal yang paling sering duplikat.
- Terapkan per-key lock, bukan lock global.
- Gunakan expiry pada lock dan pelepasan lock berbasis token pemilik.
- Tentukan perilaku saat lock gagal: wait singkat, stale response, atau requeue dengan backoff.
- Terapkan double-check cache setelah lock didapat.
- Tambahkan TTL jitter untuk mencegah expiry serentak.
- Pertimbangkan stale-while-revalidate untuk data yang tidak butuh freshness absolut.
- Buat job queue idempoten dan sedapat mungkin deduplicate sebelum enqueue.
- Batasi retry dan gunakan backoff acak agar tidak memicu storm.
- Pantau hit/miss rate, lock contention, durasi regenerasi, stale served rate, retry rate, queue lag, dan beban backend.
- Uji skenario gagal: worker crash saat memegang lock, backend lambat, invalidasi massal, dan lonjakan traffic mendadak.
Penutup
Mencegah cache stampede pada worker dan queue di Redis bukan soal satu trik tunggal, melainkan soal mengendalikan siapa yang boleh meregenerasi data, kapan refresh dilakukan, dan bagaimana worker lain bersikap saat data belum siap. Untuk kebanyakan sistem, kombinasi paling praktis adalah per-key lock + TTL jitter + stale-while-revalidate + idempotensi job + observability.
Jika harus mulai dari satu langkah yang paling berdampak, mulai dari key paling populer dan job paling mahal. Tambahkan lock yang benar, ukur contention-nya, lalu lanjutkan dengan stale fallback dan deduplikasi queue. Pendekatan bertahap seperti ini jauh lebih aman dibanding mengganti seluruh strategi cache sekaligus tanpa data operasional yang cukup.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!