Session pengguna yang terlihat acak kedaluwarsa hampir selalu bukan benar-benar acak. Dalam banyak insiden backend, masalah ini muncul karena cache TTL atau TTL session store dihitung dengan asumsi yang berbeda di tiap lapisan: aplikasi mengira satuannya detik, library mengirim milidetik, Redis menyimpan sesuai perintah yang diterima, lalu middleware membaca hasilnya seolah semuanya konsisten.
Jika gejalanya berupa pengguna tiba-tiba logout padahal baru aktif beberapa menit, fokus pertama bukan pada autentikasi dulu, melainkan pada masa hidup state. Session adalah bagian dari state kritis sistem. Sama seperti kita perlu “melindungi otak” agar fungsi inti tetap aman, sistem juga perlu melindungi state inti seperti session, token, dan cache identitas agar tidak rusak oleh asumsi kecil yang salah.
Gejala: logout terasa acak, padahal pola sebenarnya ada
Pada kasus ini, tim menerima keluhan bahwa pengguna web sering keluar sendiri. Tidak semua pengguna terdampak, tidak di semua endpoint, dan sulit direproduksi dari laporan awal. Gejala yang muncul:
- Pengguna login berhasil, lalu 1-5 menit kemudian terlihat logout tanpa aksi eksplisit.
- Sebagian request API masih valid, tetapi request berikutnya dianggap tidak punya session.
- Masalah lebih sering muncul saat trafik tinggi atau saat user berpindah halaman cepat.
- Log autentikasi tidak menunjukkan event logout dari aplikasi.
Masalah seperti ini sering menipu karena tampak seperti bug auth, load balancer, atau cookie. Padahal sumber utamanya bisa sesederhana satu angka TTL yang salah skala.
Timeline insiden dan hipotesis yang sempat keliru
Hari 1: dugaan cookie browser
Hipotesis awal biasanya mengarah ke sisi klien:
- Cookie SameSite atau Secure salah.
- Domain cookie tidak konsisten antar-subdomain.
- Waktu sistem klien melenceng.
Ini wajar, karena gejalanya tampak seperti browser “kehilangan” session. Namun setelah diperiksa, cookie session tetap ada di browser dan masih dikirim di request berikutnya.
Hari 2: dugaan load balancer atau multi-instance
Hipotesis kedua adalah masalah sticky session atau session store yang tidak tersinkron antarnode. Tim lalu memeriksa:
- Apakah semua instance aplikasi membaca Redis yang sama.
- Apakah ada failover Redis yang memotong session.
- Apakah request masuk ke node yang berbeda dengan konfigurasi session berbeda.
Hasilnya: topologi benar, session store terpusat, dan node aplikasi konsisten.
Hari 3: pola muncul pada TTL
Baru setelah log dan inspeksi key diperiksa lebih dekat, terlihat bahwa session yang seharusnya hidup 30 menit ternyata kadang hanya memiliki sisa TTL puluhan detik, bahkan ada yang habis hampir seketika setelah ditulis ulang.
Di titik ini, fokus bergeser dari autentikasi ke perhitungan expiry.
Root cause teknis: perbedaan satuan waktu dan mismatch TTL
Ada beberapa akar masalah yang umum, dan sering kali lebih dari satu terjadi bersamaan.
1. Detik vs milidetik
Ini penyebab paling klasik. Aplikasi menyimpan TTL sebagai 30 * 60 dan berasumsi artinya 1800 detik. Namun lapisan cache client tertentu bisa memiliki API yang membedakan:
- metode yang menerima detik,
- metode lain yang menerima milidetik,
- atau opsi
ttlyang semantiknya bergantung adapter.
Jika angka 1800 dikirim ke API yang menganggap milidetik, session akan hidup sekitar 1,8 detik. Sebaliknya, jika 1800000 dikirim ke API yang menganggap detik, session akan hidup jauh lebih lama dari yang dimaksud.
2. Pembulatan waktu yang merugikan
Masalah lain muncul saat expiry dihitung dari selisih timestamp:
ttl = Math.floor((expiresAt - Date.now()) / 1000)Secara teori ini benar. Tetapi jika expiresAt dihitung terlalu dekat dengan waktu eksekusi, pembulatan ke bawah bisa membuat TTL menjadi 0. Pada beberapa driver, TTL 0 berarti key langsung kedaluwarsa. Ini sering muncul saat session di-refresh pada request yang hampir menabrak batas expiry.
3. Mismatch TTL framework dan TTL store
Framework session sering punya konfigurasi sendiri, misalnya “idle timeout 30 menit” atau “rolling session”. Redis di sisi lain hanya tahu angka TTL pada key. Jika aplikasi memperbarui cookie atau metadata session tanpa benar-benar memperpanjang expiry key di store, pengguna terlihat masih login dari sisi klien, tetapi session server-side sebenarnya sudah hilang.
Contoh mismatch yang sering terjadi:
- Cookie berlaku 30 menit, tetapi key Redis hanya 5 menit.
- Middleware auth membaca session seolah sliding expiration aktif, tetapi writer session tidak melakukan touch pada tiap request.
- TTL disetel saat login saja, bukan saat aktivitas berlanjut.
4. Satuan konfigurasi yang berbeda antar-environment
Masalah makin sulit dideteksi ketika satu environment memakai menit di file konfigurasi aplikasi, sementara komponen lain membaca detik dari environment variable. Misalnya:
SESSION_TTL=30dimaksudkan sebagai menit oleh framework.- Helper internal membaca nilai yang sama dan mengirimnya langsung ke Redis sebagai detik.
Di staging mungkin tidak terasa karena trafik rendah, tetapi di production pola logout acak mulai terlihat.
Langkah investigasi yang benar-benar membantu
1. Cek log request dan lifecycle session
Jangan mulai dari log error saja. Tambahkan log terstruktur di titik berikut:
- session ID saat request masuk,
- user ID hasil resolusi session,
- event penulisan ulang session,
- TTL yang dihitung sebelum disimpan,
- hasil baca session: hit, miss, expired.
Contoh log yang berguna:
{"event":"session_write","sessionId":"s:abc123","ttlSeconds":1800,"userId":42}
{"event":"session_read","sessionId":"s:abc123","result":"hit","remainingTtlSeconds":27}
{"event":"session_read","sessionId":"s:abc123","result":"miss"}Jika TTL tertulis 1800 saat write tetapi tinggal 27 detik tak lama kemudian, ada indikasi kuat mismatch di storage layer atau overwrite dari proses lain.
2. Reproduksi lokal dengan waktu yang dipersingkat
Jangan debug session 30 menit secara manual. Turunkan TTL ke angka kecil yang mudah diamati, misalnya 60 detik. Lalu jalankan skenario berikut:
- Login dan catat session ID.
- Panggil endpoint yang hanya membaca session.
- Panggil endpoint yang seharusnya memperpanjang session.
- Periksa TTL key sebelum dan sesudah request.
Tujuannya bukan sekadar melihat session hilang, tetapi memastikan apakah TTL:
- sesuai saat dibuat,
- diperpanjang saat diakses,
- atau justru dipendekkan saat ditulis ulang.
3. Inspect key expiry langsung di Redis
Jika memakai Redis, inspeksi langsung lebih meyakinkan daripada menebak dari aplikasi.
TTL session:abc123
PTTL session:abc123
GET session:abc123TTL mengembalikan sisa waktu dalam detik, sedangkan PTTL dalam milidetik. Perbandingan ini penting untuk mengetahui apakah aplikasi menulis expiry dengan presisi milidetik atau detik.
Jika Anda melihat pola TTL sangat kecil padahal konfigurasi aplikasi besar, curigai bug satuan waktu lebih dulu sebelum menyalahkan Redis.
4. Trace middleware dan adapter cache
Jangan berhenti di layer controller atau auth. Telusuri alur lengkap:
- middleware session menerima cookie,
- session store membaca data,
- framework memutuskan apakah session perlu di-refresh,
- adapter cache mengubah TTL ke format driver,
- driver mengirim perintah expiry ke Redis.
Di sinilah bug sering tersembunyi: helper internal mengonversi menit ke detik, lalu adapter lain kembali mengasumsikan input masih menit.
5. Verifikasi dari dua sisi: aplikasi dan storage
Fix belum bisa dianggap benar jika hanya lolos uji login manual. Verifikasi harus dilakukan dari:
- sisi aplikasi: request berulang tetap mendapatkan session valid,
- sisi storage: key benar-benar memiliki TTL yang diharapkan dan berubah sesuai desain.
Contoh bug nyata pada kode
Berikut contoh pola bug yang sering terjadi pada layanan Node.js atau backend lain dengan helper custom. Intinya relevan lintas bahasa: nilai TTL diperlakukan dengan satuan berbeda.
Implementasi bermasalah
// Konfigurasi aplikasi: 30 menit
const SESSION_TTL_MINUTES = 30;
function persistSession(redis, key, value) {
// Developer A mengubah menit ke detik
const ttl = SESSION_TTL_MINUTES * 60;
// Developer B mengira API menerima milidetik lalu mengganti metode
return redis.psetex(key, ttl, JSON.stringify(value));
}Masalahnya: psetex menggunakan milidetik. Nilai 1800 menjadi 1,8 detik.
Implementasi yang benar
const SESSION_TTL_MINUTES = 30;
const SESSION_TTL_SECONDS = SESSION_TTL_MINUTES * 60;
const SESSION_TTL_MS = SESSION_TTL_SECONDS * 1000;
function persistSession(redis, key, value) {
// Pilih salah satu API dan konsisten dengan satuannya
return redis.setex(key, SESSION_TTL_SECONDS, JSON.stringify(value));
// Alternatif jika memang perlu milidetik:
// return redis.psetex(key, SESSION_TTL_MS, JSON.stringify(value));
}Yang penting bukan metode mana yang dipakai, melainkan konsistensi satuan di seluruh jalur kode.
Kasus pembulatan yang memicu expiry terlalu cepat
function ttlFromExpiry(expiresAtMs) {
// Riskan: pembulatan ke bawah bisa menghasilkan 0
return Math.floor((expiresAtMs - Date.now()) / 1000);
}Perbaikan yang lebih aman:
function ttlFromExpiry(expiresAtMs) {
const ttl = Math.ceil((expiresAtMs - Date.now()) / 1000);
return Math.max(ttl, 1);
}Math.ceil dan batas minimum 1 membantu mencegah session langsung hilang hanya karena selisih sub-detik.
Mengapa bug ini terasa “acak” di production
Secara teknis bug-nya deterministik, tetapi dari sudut pandang pengguna terlihat acak karena dipengaruhi banyak kondisi:
- waktu antar-request berbeda-beda,
- sebagian endpoint menulis ulang session, sebagian tidak,
- race condition antar-request paralel bisa meng-overwrite session dengan TTL berbeda,
- pengguna aktif memperpanjang session pada alur tertentu, tetapi tidak pada alur lain.
Contoh race condition: request A dan request B memakai session sama. Request A membuat TTL baru 30 menit. Request B, yang memuat snapshot lama atau menghitung TTL dengan bug, menulis ulang session sesudahnya dengan TTL 30 detik. Dari sisi pengguna, session seolah hidup normal lalu mati mendadak.
Perbaikan yang sebaiknya dilakukan
1. Pusatkan definisi durasi
Jangan menyebarkan literal angka waktu di banyak file. Buat satu sumber kebenaran:
const SESSION_TTL = {
minutes: 30,
seconds: 30 * 60,
milliseconds: 30 * 60 * 1000,
};Lalu pakai nama variabel yang eksplisit, bukan ttl = 1800 tanpa konteks.
2. Bungkus akses cache/session dengan API internal yang tegas
Daripada membiarkan banyak bagian aplikasi memanggil Redis langsung, buat wrapper:
function setSession(redis, key, value, ttlSeconds) {
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
throw new Error('ttlSeconds harus integer positif');
}
return redis.setex(key, ttlSeconds, JSON.stringify(value));
}Ini memberi satu pintu validasi dan mengurangi peluang salah pakai API.
3. Pastikan strategi expiry sesuai desain produk
Tentukan sejak awal:
- Apakah session memakai absolute expiration?
- Apakah harus sliding expiration saat user aktif?
- Apakah cookie dan session store harus punya masa hidup identik?
Tanpa keputusan desain yang jelas, tim mudah membuat implementasi yang tampak benar tetapi saling bertentangan.
Guardrail testing yang wajib ada
1. Unit test untuk konversi satuan
it('menggunakan TTL session dalam detik saat memanggil Redis', async () => {
const redis = { setex: jest.fn() };
await setSession(redis, 'session:1', { userId: 1 }, 1800);
expect(redis.setex).toHaveBeenCalledWith('session:1', 1800, JSON.stringify({ userId: 1 }));
});Test sederhana seperti ini sering cukup untuk menangkap bug konversi yang mahal di production.
2. Integration test dengan inspeksi expiry
Jika memungkinkan, jalankan Redis sungguhan di test environment dan verifikasi:
- key dibuat,
- TTL berada dalam rentang yang diharapkan,
- TTL bertambah saat sliding session aktif,
- session benar-benar hilang setelah masa berlaku lewat.
3. Test race condition dasar
Buat skenario dua request paralel yang memperbarui session yang sama. Pastikan writer terakhir tidak menurunkan TTL secara tak sengaja karena membawa metadata lama.
Alert dan observability yang berguna
Masalah session expiry sering lolos karena belum ada metrik yang tepat. Beberapa sinyal yang layak dipantau:
- lonjakan session miss rate,
- rasio login sukses yang segera diikuti 401/403,
- distribusi TTL key session yang terlalu pendek,
- jumlah penulisan session dengan TTL nol atau negatif,
- perubahan tajam pada volume reauthentication.
Tambahkan juga log atau metric saat aplikasi hendak menulis TTL yang di luar batas aman:
if (ttlSeconds < 60 || ttlSeconds > 86400) {
logger.warn({ ttlSeconds, key }, 'session TTL di luar rentang normal');
}Alert seperti ini murah, tetapi sering menjadi sinyal pertama sebelum pengguna ramai melapor.
Checklist pencegahan bug cache TTL salah
- Gunakan nama variabel durasi yang eksplisit:
Seconds,Ms,Minutes. - Jangan campur API Redis detik dan milidetik tanpa wrapper yang jelas.
- Samakan kebijakan expiry di cookie, middleware session, dan session store.
- Hindari pembulatan ke bawah yang bisa menghasilkan TTL 0.
- Tambahkan test untuk konversi satuan waktu dan sliding expiration.
- Log nilai TTL saat session dibuat atau diperbarui.
- Monitor session miss rate dan pola logout mendadak.
- Review race condition jika ada request paralel yang menulis state sama.
Penutup
Debug cache TTL salah bukan sekadar urusan angka expiry. Ini soal menjaga state kritis sistem tetap utuh. Session yang tampak logout acak sering berakar pada detail kecil: detik vs milidetik, pembulatan yang salah, atau framework dan Redis yang punya pemahaman waktu berbeda.
Pendekatan paling efektif adalah memperlakukan session seperti komponen inti yang harus “dilindungi”: definisi TTL dipusatkan, wrapper dibuat tegas, expiry diuji, dan observability dipasang sejak awal. Dengan begitu, bug yang terlihat acak bisa diubah menjadi masalah yang cepat dikenali, mudah direproduksi, dan aman diperbaiki.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!