D
P
0
← Semua artikel Read in English

Next.js & React di Produksi

Data Cache Next.js Tetap Menyerahkan 200 Basi Saat API Hulu Membalas 401: Harga Seminggu Lalu Tercap 'Updated 2m ago'

· · 9 menit baca
Data Cache Next.js Tetap Menyerahkan 200 Basi Saat API Hulu Membalas 401: Harga Seminggu Lalu Tercap 'Updated 2m ago'

API harga hulu yang dipakai situs data kripto klien menjawab setiap panggilan dengan 401, dan selama dua hari pertama saya menganggap akibatnya sederhana: harga di situs beku sejak 4 Oktober 2026 sekitar pukul 10.05 UTC. Anggapan itu salah, dan saya mengetahuinya secara kebetulan.

Pada 6 Oktober pukul 14.35 WIB, saya sedang mengecek perbaikan label umur harga di peramban. Halaman koin teratas, yang harganya dirender di server, menampilkan $83,993 dengan label "Updated 2m ago". Padahal angka beku 4 Oktober untuk koin itu 85.231 dolar, dan harga nyatanya saat itu 85.381 dolar. Yang tampil bukan salah satu dari keduanya, dan labelnya mengaku baru diperbarui dua menit sebelumnya.

Angka 83.993 itu ternyata harga 29 September. Sebagai pembanding, harga publik koin itu pada 29 September pukul 07.00 UTC adalah 83.976 dolar. Jadi di tengah outage, harga di halaman itu tidak beku. Dia mundur seminggu, lalu diberi cap waktu baru. Di produksi, HTML dan JSON-LD halamannya sudah membawa 83.993, walaupun pengunjung koin teratas siang itu masih melihat angka dari feed harga, 85.231 dengan label "Updated 45h ago".

Data cache yang tidak pernah melihat 401

Panggilan details untuk tiap koin membawa deskripsi, tabel bursa, dan data pasar sekaligus. Sebelum perbaikan, opsinya berisi next: { revalidate: 1800 }. Bentuknya kira-kira begini, disederhanakan dan dengan nama yang saya netralkan:

// disederhanakan, nama dinetralkan: panggilan details sebelum perbaikan
const res = await upstreamFetch(detailsUrl, {
  headers,
  next: { revalidate: 1800 },
});
const coin = await res.json();
 
// separuh details (deskripsi, tabel bursa) DAN snapshot pasar,
// dua-duanya dicap dengan waktu penulisan
const now = new Date().toISOString();
await writeSnapshot(coin, { stampedAt: now, fetchedAt: now });

Nilai 1800 berarti entri data cache dianggap basi setelah 30 menit. Di Next 16, entri basi tidak membuat panggilan menunggu hulu. Next menyerahkannya apa adanya lalu merevalidasi di latar belakang, dan hasil revalidasi itu baru dipakai oleh panggilan berikutnya. Panggilan details untuk satu koin hanya terjadi seminggu sekali, jadi setiap kali dia datang, yang dia terima selalu jawaban panggilan sebelumnya, yang umurnya seminggu. Jawaban itu lalu ditulis ke Redis dengan stampedAt dan fetchedAt berisi waktu penulisan. Yang ikut bukan hanya deskripsi dan tabel bursa, tetapi seluruh snapshot pasar, harga termasuk di dalamnya. Akibatnya, stampedAt pada snapshot koin adalah waktu penulisan, bukan waktu datanya.

Saat hulu menolak, revalidasi di latar menerima 401, tetapi Next tetap menyerahkan 200 terakhir yang dia simpan. Kodenya tidak pernah melihat 401 itu, hanya 200 dari cache. Mekanisme ini belakangan dikonfirmasi dalam review perbaikan akar dengan menjalankan patched fetch milik Next sendiri. Cache di panggilan ini pun sebenarnya sisa desain Juni, ketika details diambil per tampilan halaman dan cache Next berfungsi sebagai rem. Sejak rework Agustus menaruh details di Redis selama seminggu di belakang batas panggilan per jam, cache itu berhenti menghemat apa pun.

Kenapa cacat ini tersembunyi sebelum outage

Dalam keadaan normal, pass harga setiap 30 menit, sebuah panggilan bulk tanpa cache, menimpa separuh pasar dalam setengah jam. Yang tertinggal satu siklus hanya deskripsi dan tabel bursa, dan tidak ada yang menyadarinya. Saat outage, tidak ada yang menimpa. Begitu separuh details sebuah koin kedaluwarsa setelah TTL 7 hari, backfill "menyegarkannya" dari cache, dan harga halaman kembali ke harga pengambilan details sebelumnya, lengkap dengan label segar.

Urutan itulah yang terjadi pada koin-koin teratas. Details mereka diambil bersamaan pada 29 September pukul 07.28 UTC, jadi kedaluwarsa bersamaan tujuh hari kemudian, 6 Oktober pukul 14.28 WIB. Deskripsi koin-koin itu memang hilang pukul 14.28, lalu kembali pukul 14.33 dalam versi seminggu lalu. Snapshot koin teratas membawa harga 83.993 dengan cap 2026-10-06T07:33:06Z, fetchedAt separuh details-nya jatuh di detik yang sama, dan TTL-nya kembali 7 hari penuh. Cap itu sama dengan 14.33 WIB, dua menit sebelum saya membuka halamannya. Dari situlah "Updated 2m ago" berasal.

Penulisan ulang yang menyebar setiap pass

Pukul 14.40 WIB, 34 dari 12.944 snapshot koin membawa cap dari dalam masa outage. Sembilan belas di antaranya datang dari satu pass pukul 07.33 UTC, lima menit sesudah details koin-koin teratas tadi kedaluwarsa, dan 2 lagi dari pass 07.02 UTC. Sesudah itu, setiap pass 30 menit menambah jumlahnya:

6 Okt, WIB    snapshot koin bercap dari dalam outage
14.40         34 dari 12.944
15.30         54     pass 08.03 UTC menambah 20
16.08         81     sekitar 20 sampai 27 lagi setiap pass
17.28         105    semuanya bertanda panggilan details, nol yang tidak
17.36         112    pass terakhir build lama (10.36 UTC) menulis ulang 7 lagi
17.43.06      perbaikan akar tayang di produksi
18.06.40      pass pertama build baru dimulai: tidak menulis apa pun, tetap 112
18.37.00      pass kedua dimulai: tidak menulis apa pun, tetap 112

Tanda di tabel itu adalah cap snapshot yang sama persis dengan fetchedAt separuh details-nya. Pemindai baca-saja menunjukkan ke-105 snapshot pada 17.28 WIB membawa tanda itu, nol yang tanpa tanda, dan ke-112 di akhir pun bertanda semua, jadi penulis ulangnya hanya panggilan details. Selisih harganya tidak kecil: satu koin yang tercap ulang meleset 12%, satu lagi 16%. Karena penulisan ulang terus menyebar setiap pass, perbaikan akarnya dirilis sendirian lebih dulu, tanpa menunggu perbaikan label.

Perbaikan akar: details tanpa cache, 401 tidak menulis apa pun

Perbaikan akar membuat tiga panggilan, yaitu details koin, details koleksi NFT, dan ticker NFT, meminta respons tanpa cache, dan panggilan yang ditolak kini tidak menulis data koin atau koleksi apa pun. Commit yang sama menambah tes yang mensimulasikan jawaban 401 dan mengharuskan fetcher details koin dan NFT tidak menulis apa-apa. Lagi-lagi disederhanakan, dengan nama netral:

// disederhanakan, nama dinetralkan: panggilan details sesudah perbaikan
const res = await upstreamFetch(detailsUrl, { headers, next: { revalidate: 0 } });
 
// bentuk tes yang menyertainya
it("writes nothing when the upstream refuses the call", async () => {
  // fetch tiruan menjawab 401: new Response(body, { status: 401 })
  // ...fetcher details koin dan NFT dijalankan...
  expect(writes).toEqual([]);
});

Menurut pesan commit-nya, batas per jam tetap 60 details koin dan 20 details NFT, dan hit cache yang basi pun sudah mengirim satu panggilan di latar, jadi sebuah refresh memakan jumlah panggilan yang sama seperti dulu.

Perbaikan ini juga menutup jalan pintas yang tidak disengaja. Penulisan ulang membuat snapshot yang tercap ulang lolos dari batas umur snapshot 7 hari, sehingga tanpa perbaikan, koin-koin itu akan tetap tayang dengan harga salah. Dengan perbaikan, halaman koin menjawab not-found begitu batas itu lewat sementara hulu masih mati. Yang tidak dikerjakan perbaikan ini adalah memulihkan ke-112 snapshot yang sudah telanjur tercap ulang. Dia hanya menghentikan yang baru.

Alasan melepas cache ini sebenarnya sudah pernah dipakai, hanya tidak merata. Tier prioritas yang ditulis 1 Oktober sudah meminta panggilan details tanpa cache untuk koin-koinnya sendiri dengan alasan yang persis sama, tetapi alasan itu tidak diterapkan ke semua koin lain.

Label umur yang ikut percaya pada cap

Perbaikan label yang sedang saya cek siang itu punya masalahnya sendiri. Versi awalnya membuat bagian hero harga di halaman koin memercayai detail.stampedAt, dan review atas versi itu menemukan satu blocker yang dikonfirmasi lewat pengujian di peramban: tanpa feed, build tersebut mencetak "$83,993 Updated 41m ago" di halaman koin teratas, harga seminggu lalu di bawah label segar, karena 34 snapshot membawa cap yang salah.

Versi yang dirilis membedakan dua penulis snapshot pasar. Pass harga mencap snapshot dengan waktu saat dia meminta harga. Panggilan details menulis snapshot bersama separuh details di bawah satu pembacaan jam, jadi cap yang sama persis adalah tanda penulis itu, dan snapshot bertanda dari masa sebelum perbaikan tidak diberi cap sama sekali sampai pass harga menulisnya ulang. Uji label end-to-end di halaman koin teratas menunjukkan "$85,231 Updated 2d ago" saat halaman disajikan seperti biasa, dan "$83,993 Delayed" saat feed hilang. Harganya masih harga lama, tetapi halamannya berhenti mengaku segar. Perubahan ini tayang di produksi pukul 19.14.55 WIB, sekitar satu setengah jam sesudah perbaikan akar.

Cacat kembar di pass NFT dan panggilan listing

Perbaikan akar hanya menyentuh tiga panggilan tadi. Cacat yang sama masih ada di pass pasar NFT dan di panggilan listing, termasuk daftar top-100 yang ditulis ulang setiap 15 menit dari cache basi, yang juga menjaga fallback ticker tetap hidup.

Untuk NFT, pengukuran baca-saja menunjukkan 1.181 dari 1.320 snapshot dicap ulang setiap 30 menit di atas floor 4 Oktober, karena pass NFT menulis baris yang dia dapat dari cache dengan cap baru. Pembacaan kodenya berujung di tempat yang sama: entri basi di data cache Next disajikan sebagai 200 selamanya selama hulu menolak, dan bertahan melewati rilis. Setelah perbaikan NFT tayang pada 7 Oktober, dua pass build baru di produksi menerima 401, membuat satu panggilan list, dan tidak menulis apa pun, lalu capnya berhenti di 15.12.15 WIB.

Panggilan listing menyusul pada 12 Oktober. Pesan commit-nya menyebut semua fetcher list, kartu, dan grafik memanggil hulu lewat data cache Next dengan revalidate seumur hidupnya sendiri, lalu menyimpan jawabannya ke Redis sebagai data baru, sehingga daftar 4 Oktober disajikan sebagai data terkini berhari-hari kemudian. Di sini revalidate: 0 sengaja tidak dipakai, karena Next menganggap revalidate: 0 dan no-store sebagai alasan untuk merender halaman secara dinamis. Panggilan yang terjadi di tengah regenerasi latar sebuah halaman, seperti halaman pasar atau beranda, akan mengakhiri regenerasi itu dengan galat "Page changed from static to dynamic at runtime". Jadi opsinya dikirim tanpa next dan tanpa cache:

// disederhanakan, nama dinetralkan: opsi fetch untuk fetcher list, kartu, dan grafik
function withoutDataCache(init: RequestInit): RequestInit {
  const { next, cache, ...rest } = init; // dua field ini sengaja dibuang
  return rest; // headers, signal, method, dan sisanya lewat apa adanya
}

Menurut catatan di kodenya, fetch tanpa kedua field itu tidak disimpan dan tidak dijawab dari data cache oleh Next 16 setelah build selesai, perilaku yang di patch-fetch.js disebut "auto no cache". Untuk perubahan ini saya belum punya verifikasi khusus seperti dua pass kosong sesudah perbaikan akar, jadi saya belum menganggapnya terbukti di produksi.

Cap waktu harus milik data, bukan milik penulisan

Prinsipnya pendek: cache di depan panggilan yang jawabannya ditulis ke penyimpanan di bawah waktu "sekarang" mengubah jawaban lama menjadi fakta baru. Aturan yang saya pegang sekarang ada dua. Setiap panggilan hulu yang hasilnya disimpan dengan cap waktu harus tanpa cache (revalidate: 0) atau membawa waktu milik hulu sendiri, dan kalau panggilan itu bisa terjadi di tengah regenerasi halaman, bentuk tanpa cache-nya adalah fetch tanpa next dan cache. Lalu, kalau angka di situs berubah saat outage, jangan menganggap outage membekukannya. Baca harga snapshot di samping capnya, dan bandingkan dengan nilai baik terakhir yang diketahui.

Ini juga lain jenis dengan fallback yang mengarang angka. Tidak ada angka karangan di sini, karena 83.993 memang harga koin itu pada 29 September. Yang salah hanya umurnya, dan umur itu datang dari cap yang diisi saat penulisan. Kasus ini pun kebalikan dari force-dynamic yang diam-diam menjadikan fetch no-store. Di sana masalahnya fetch yang tidak pernah kena cache, sedangkan di sini fetch yang kena cache padahal hasilnya akan dicap sebagai baru.