Benerin Race Condition Refresh JWT di Banyak Tab Browser
Banyak tab yang refresh JWT yang sama bareng-bareng bikin duplicate call dan logout random. Ini cara benerin pakai Web Locks API dan beberapa guard.

Gue pernah dapet tiket support yang bikin bingung. User bilang aplikasinya logout sendiri random, tapi cuma kalau dia buka di dua tab sekaligus. Satu tab aja, gak pernah masalah. Dua tab, tiap beberapa menit tiba-tiba kelempar balik ke halaman login. Gue coba reproduce di satu tab, ditunggu selama apa pun, gak pernah kejadian.
Begitu gue buka aplikasinya di dua tab bersebelahan sambil merhatiin network panel, langsung ketauan. Kedua tab sama-sama nyadar access token-nya mau expired di detik yang hampir bareng. Kedua tab nembak request refresh. Salah satu menang dan dapet token baru. Tab yang satunya, yang masih megang token lama di memory, nyoba pakai token itu sesaat kemudian, ditolak, terus nganggep penolakan itu sebagai auth failure beneran. User-nya kelogout gara-gara tab keduanya sendiri.
Key Takeaways
- Banyak tab yang kebuka dari aplikasi yang sama bisa masing-masing mendeteksi token expired dan nyoba refresh bareng-bareng, bikin duplicate refresh call dan logout palsu.
- Web Locks API, yang bisa diakses lewat
navigator.locks, bikin cuma satu tab yang beneran ngerjain refresh sementara tab lain nunggu, dan aman kalau tab-nya crash. - Selalu baca ulang token dari storage setelah dapet lock, soalnya tab lain mungkin udah refresh duluan pas lo lagi nunggu.
- Tambahin in-tab concurrency guard biar interval timer dan visibility change event gak masing-masing nembak refresh sendiri-sendiri di tab yang sama.
- Delay singkat sebelum call pertama setelah login berguna kalau session store di backend nulisnya asynchronous.
Apa Penyebab Race Condition Refresh JWT di Banyak Tab?
Race condition refresh JWT kejadian kalau dua tab browser atau lebih dari aplikasi yang sama mendeteksi access token yang mau atau udah expired di waktu yang hampir bareng, terus masing-masing mulai request refresh sendiri-sendiri. Tab browser share origin yang sama dan sering share storage yang sama juga, tapi mereka jalan di JavaScript execution context yang terpisah tanpa koordinasi bawaan di antara mereka.
Tiap tab biasanya punya timer sendiri atau request interceptor sendiri yang ngawasin response 401. Begitu window expiry token-nya tiba, beberapa tab bisa lewatin garis itu dalam hitungan milidetik satu sama lain. Gak ada yang nyegah dua tab buat masing-masing mutusin, secara independen, kalau sekarang saatnya manggil endpoint refresh.
Kenapa Ini Cuma Muncul Kalau Buka Banyak Tab?
Ini cuma muncul kalau ada banyak tab soalnya satu tab gak punya timer saingan buat dilawan. Bug kayak gini emang terkenal susah ketangkep di QA normal, soalnya kebanyakan testing manual dilakuin di satu tab, dan automated end to end test hampir gak pernah sengaja buka session yang sama dua kali.
Pola failure-nya juga gak konsisten dari sononya. Kadang kedua refresh call-nya berhasil dan satu token overwrite yang lainnya gitu aja, diam-diam ngebikin invalid token lama yang masih dipegang tab yang satunya di memory. Kadang refresh call kedua ditolak langsung soalnya server udah rotate refresh token pas call pertama. Gimana pun, pengalaman user-nya sama, tab yang keliatan rusak tanpa alasan yang jelas.
Gimana Cara Nyegah Banyak Tab Refresh Bareng-Bareng?
Caranya pakai Web Locks API biar cuma satu tab yang beneran ngerjain refresh sementara semua tab lain nunggu kerjaan itu selesai. Web Locks API itu browser API, diakses lewat navigator.locks, yang bikin script yang jalan di tab, window, atau worker berbeda dari origin yang sama bisa koordinasi akses ke shared resource berdasarkan nama.
async function refreshAccessToken() {
return navigator.locks.request("auth-token-refresh", async () => {
const currentToken = readStoredToken();
if (!isExpiringSoon(currentToken)) {
return currentToken;
}
const response = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
});
if (!response.ok) {
throw new Error("Token refresh failed");
}
const { accessToken } = await response.json();
storeToken(accessToken);
return accessToken;
});
}Baris pentingnya adalah yang baca ulang token tepat setelah lock-nya didapet dan ngecek apakah token itu masih mau expired. Kalau tab kedua minta lock yang sama pas tab pertama lagi refresh, dia bakal dapet lock-nya cuma setelah tab pertama selesai, dan pas itu token di storage udah fresh. Pengecekan itu ngubah duplicate refresh yang keantri jadi no-op murah, bukan network call kedua beneran.
Gimana Kalau Ada Tab yang Crash di Tengah Refresh?
Di sinilah Web Locks API dapet tempatnya dibanding solusi bikinan sendiri. Pola umum sebelum ada Web Locks adalah mutex flag yang ditulis ke localStorage, di mana satu tab set flag sebelum refresh dan clear setelahnya. Pendekatan itu rusak begitu tab-nya crash, ke-force-close, atau kehilangan power pas flag-nya masih ke-set, soalnya gak ada yang bakal clear flag itu lagi. Semua tab lain jadi nunggu selamanya buat lock yang gak bakal pernah dilepas.
navigator.locks gak punya failure mode kayak gitu. Lock-nya terikat sama execution context yang minta, jadi kalau tab yang megang lock-nya ketutup atau crash, browser bakal lepas lock-nya otomatis dan tab yang nunggu berikutnya langsung ambil alih. Per 2026, navigator.locks udah didukung di Chrome dan browser Chromium-based lainnya. Kalau audience lo ada yang pakai browser tanpa dukungan itu, tetep pakai feature check dan fallback ke strategi refresh single-tab yang lebih simpel, jangan asumsiin API-nya universal.
Gimana Cara Handle Refresh Trigger yang Overlap di Tab yang Sama?
Caranya pakai in-tab concurrency guard yang ngegabungin semua refresh trigger jadi satu in-flight request. Cross-tab locking nyelesain masalah antar tab, tapi satu tab masih bisa trigger refresh dari lebih dari satu tempat, misalnya interval timer dan visibility change listener yang nembak di detik yang sama pas user balik ke tab-nya.
let refreshInFlight = null;
function requestTokenRefresh() {
if (!refreshInFlight) {
refreshInFlight = refreshAccessToken().finally(() => {
refreshInFlight = null;
});
}
return refreshInFlight;
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
requestTokenRefresh();
}
});
setInterval(requestTokenRefresh, TOKEN_CHECK_INTERVAL_MS);Caller mana pun yang trigger refresh, entah itu interval, visibility listener, atau API call yang gagal, bakal dapet in-flight promise yang sama, bukan mulai yang kedua. Digabung sama cross-tab lock, ini artinya refresh cuma kejadian sekali per tab dan sekali di semua tab dalam satu waktu.
Apa Lagi yang Dibutuhin Selain Locking?
Lo juga butuh retry path yang baca token yang beneran fresh, bukan yang stale, dan kebijakan yang konsisten buat mutusin apa arti auth failure sebenernya. Request yang gagal dengan 401 gak boleh langsung diasumsiin session-nya mati. Dia harus trigger coordinated refresh lewat fungsi yang sama yang di-lock, terus retry sekali pakai token apa pun yang balik dari refresh itu.
async function apiRequest(path, options = {}) {
let token = readStoredToken();
let response = await fetch(path, withAuthHeader(options, token));
if (response.status === 401) {
token = await requestTokenRefresh();
response = await fetch(path, withAuthHeader(options, token));
}
return response;
}Perhatiin kalau retry-nya manggil readStoredToken lagi secara gak langsung lewat requestTokenRefresh, bukan pakai ulang token yang ke-capture pas request awalnya dibikin. Bedanya ini penting lebih dari keliatannya. Kalau request awal dibikin tiga puluh detik sebelum beneran nembak, gara-gara ada request yang keantri atau network lambat, token yang ke-capture itu udah bisa stale pas retry-nya kejadian, dan retry pakai token yang udah lo tau lama cuma bakal ngasilin 401 lagi.
Satu edge case lagi yang perlu dihandle secara eksplisit, tepat setelah login, sebagian backend nulis session ke fast store, kayak in-memory cache, secara asynchronous. Kalau call pertama yang authenticated lo nembak sebelum tulisan itu landing, lo dapet 401 palsu di token yang sebenernya valid. Delay singkat sebelum call pertama setelah login itu, sekitar beberapa ratus milidetik, cara murah buat nyerap lag itu tanpa nambah kompleksitas ke retry logic-nya sendiri.
Naive Multi-Tab Auth vs Coordinated Refresh
| Aspek | Pendekatan naive | Pendekatan coordinated |
|---|---|---|
| Trigger refresh | Tiap tab refresh sendiri-sendiri | Cuma tab yang megang lock yang refresh |
| Crash safety | Mutex flag localStorage bisa nyangkut selamanya | navigator.locks lepas otomatis kalau tab-nya ketutup |
| Sumber token buat retry | Token yang ke-capture pas request dibikin | Token fresh yang dibaca setelah coordinated refresh selesai |
| Trigger duplikat di tab sama | Timer dan visibility listener bisa sama-sama nembak | Digabung jadi satu in-flight refresh promise |
| Dampak race ke user | Logout palsu random, susah direproduce | No-op diam-diam buat tab yang kalah race-nya |
Ini bukan pertama kali baca sinyal beneran lebih penting daripada react lebih keras. Gue nemuin pelajaran yang sama dari sudut yang beda pas handle 429 rate limit di bulk API request, di mana solusinya adalah baca response header, bukan retry asal-asalan.
Best Practices buat Multi-Tab Token Refresh
- Bungkus refresh call beneran di dalam blok
navigator.locks.request(), pakai lock name yang stabil dan sama di seluruh aplikasi. - Cek ulang expiry token-nya langsung setelah dapet lock, sebelum bikin network call apa pun.
- Gabungin trigger di tab yang sama jadi satu in-flight promise biar interval sama visibility listener gak pernah sama-sama mulai refresh.
- Retry request yang gagal pakai token yang baru dibaca, bukan token yang ke-capture pas request-nya dibikin.
- Tambahin delay singkat sebelum call pertama yang authenticated setelah login kalau backend lo nulis session state secara asynchronous.
- Feature-detect
navigator.locksdan fallback ke strategi yang lebih simpel buat browser yang gak dukung.
Kalau lo juga ngurusin form yang ada di downstream dari session yang authenticated, sisi validasinya itu topik lain yang worth dibaca sendiri, ada di panduan password validation di React dengan Chakra UI dan React Hook Form ini.
Pertanyaan yang Sering Diajukan
Apa penyebab race condition refresh JWT di banyak tab browser?
Ini kejadian kalau dua tab atau lebih dari aplikasi yang sama masing-masing mendeteksi access token yang mau expired dan mulai refresh request sendiri di waktu yang hampir bareng, soalnya tab jalan di JavaScript context yang terpisah tanpa koordinasi bawaan.
navigator.locks didukung di semua browser?
Per 2026, navigator.locks didukung di Chrome dan browser Chromium-based lainnya. Cek "locks" in navigator dulu sebelum ngandelin itu, dan siapin fallback buat browser yang gak dukung.
Kenapa navigator.locks lebih bagus dibanding flag localStorage buat ini?
Mutex flag localStorage bisa nyangkut selamanya kalau tab yang set flag itu crash atau ketutup sebelum sempet clear, blocking semua tab lain. Lock yang didapet lewat navigator.locks terikat sama execution context tab-nya dan lepas otomatis kalau tab itu ketutup atau crash.
Masih perlu retry logic gak kalau udah ada locking?
Masih perlu. Locking nyegah duplicate refresh call, tapi request masih bisa gagal dengan 401 sebelum token-nya di-refresh. Lo butuh retry path yang minta coordinated refresh terus retry sekali pakai token fresh yang dibalikin dari situ.
Pola yang sama bisa dipake buat session token yang bukan JWT?
Bisa. Masalah koordinasinya soal kredensial apa pun yang expired dan butuh refresh yang sinkron di banyak tab, bukan spesifik soal JWT doang. Pola lock-and-retry yang sama bisa dipake buat opaque session token, API key dengan TTL pendek, atau client apa pun yang share stored credential di banyak tab.
Kesimpulan
Bug yang mulain ini sebenernya bukan soal token yang expired. Token emang seharusnya expired. Bug beneran-nya adalah gak ada yang koordinasiin apa yang kejadian pas dua tab mendeteksi expiry itu bareng-bareng. navigator.locks ngasih koordinasi itu gratis, dan lebih aman dibanding apa pun yang lo bikin sendiri pakai localStorage, soalnya gak bisa nyangkut kalau tab-nya mati di tengah refresh.
Kalau aplikasi lo support banyak tab kebuka sekaligus, yang hampir semua aplikasi emang begitu, entah direncanain atau enggak, perlakuin token refresh sebagai shared resource dari hari pertama, daripada baru debug tiket logout random enam bulan kemudian.
![Cara Handle 429 Rate Limit di Bulk API Request [2026]](https://cdn.asepalazhari.com/images/articles/development/bulk-api-429-rate-limit-retry-adaptive-pacing.jpeg)

![React Query Stale Data: Kenapa Tampil Data Lama & Cara Fixnya [2026]](https://cdn.asepalazhari.com/images/articles/development/react-query-stale-data-issue.png)