D
P
0

Next.js

`plan_limit_reached` 402 dari Sanity Bikin Semua Halaman Jadi Cloudflare 522 Sehari Setelah Launch

31 Juli 2026·4 menit baca
`plan_limit_reached` 402 dari Sanity Bikin Semua Halaman Jadi Cloudflare 522 Sehari Setelah Launch

Sehari setelah situs klien resmi launch, saya bangun dengan notifikasi yang bikin perut mules: hampir semua halaman konten tidak bisa dibuka. Yang muncul cuma layar 522 dari Cloudflare, "Connection timed out", setelah browser menggantung sekitar 90 detik. Tapi ada satu keanehan yang langsung menonjol. Homepage tetap sehat, cepat, mulus. Hanya halaman detail konten yang tumbang satu per satu. Dan sitemap yang harusnya berisi sekitar 1.800 URL malah mentok di 35 URL saja.

Kombinasi itu yang bikin saya garuk-garuk kepala. Kalau server benar-benar mati, homepage juga harusnya ikut jatuh. Tapi homepage jalan sempurna sementara halaman dinamis kena 522 semua. Sesuatu yang selektif sedang terjadi, dan itu jarang berarti "server mati total".

Jalan buntu yang saya coba dulu

Refleks pertama saya, seperti biasa, salah sasaran. Karena yang muncul layar Cloudflare, saya langsung menuduh Cloudflare. Saya cek status page mereka, bersih. Saya matikan proxy oranye sebentar biar request langsung ke origin, tetap menggantung 90 detik lalu timeout. Berarti bukan edge Cloudflare-nya yang bermasalah, dia cuma pembawa pesan.

Tersangka kedua: hosting aplikasi di Railway. Saya buka dashboard, aplikasi hidup, tidak ada crash loop, memori aman, tidak ada restart mendadak. Log aplikasi juga tidak menunjukkan exception yang fatal. Saya redeploy sekali untuk jaga-jaga, hasilnya sama saja. Halaman detail tetap menggantung sampai Cloudflare menyerah.

Di titik ini saya berhenti menebak dan mulai bertanya: kenapa homepage beda nasib? Jawabannya jadi petunjuk besar. Homepage kebetulan sudah ter-cache di edge Cloudflare, jadi ia disajikan tanpa pernah menyentuh origin. Halaman detail konten tidak ter-cache, jadi setiap request memaksa render penuh di server, dan render itulah yang menggantung. Artinya masalahnya ada di sesuatu yang dilakukan render, bukan di jaringan.

Akar masalahnya

Setiap render halaman detail menarik datanya dari Sanity. Jadi saya lakukan hal paling dasar yang harusnya saya lakukan dari awal: memanggil API Sanity langsung dengan curl, melewati seluruh lapisan aplikasi. Jawabannya langsung membuka semuanya:

{"error":"plan_limit_reached","statusCode":402,"message":"API Requests quota limit reached"}

HTTP 402, plan_limit_reached. Kuota API-nya habis. Beberapa menit kemudian saya sadar Sanity juga sudah mengirim email peringatan kuota 100 persen yang tenggelam di antara notifikasi lain semalam.

Angkanya masuk akal begitu saya cek. Tier gratis Sanity memberi jatah sekitar 250 ribu request API per bulan. Masalahnya, staging dan produksi berbagi satu dataset yang sama, dan tiap render halaman menembakkan cukup banyak query. Traffic launch, ditambah bot dan crawler yang datang bareng, plus staging yang masih aktif, menguras jatah sebulan hanya dalam hitungan jam.

Tapi kuota habis saja tidak menjelaskan kenapa halaman menggantung 90 detik penuh alih-alih gagal cepat. Di sinilah kesalahan konfigurasi saya memperparah keadaan. Klien Sanity saya dibuat dengan useCdn: false dan, yang lebih fatal, tanpa timeout sama sekali:

export const client = createClient({
  projectId,
  dataset,
  apiVersion: "2024-01-01",
  useCdn: false, // selalu ke API langsung, kena bucket kuota utama
  // tidak ada timeout: fetch menggantung tanpa batas
});

Ketika API membalas 402, koneksi tidak langsung ditutup rapi di semua jalur. Fetch tanpa timeout itu menggantung menunggu balasan yang tidak kunjung berguna, dan karena tidak ada batas, ia menunggu sampai Cloudflare sendiri yang menyerah di 90 detik dan mengembalikan 522. Jadi 522 itu cuma gejala. Penyakit aslinya adalah 402 yang tidak pernah saya tangani.

Ada efek domino kedua yang lebih halus. Salah satu helper, getApiSettings(), membaca konfigurasi dari Sanity. Saat kena 402, ia mengembalikan null, dan beberapa fetcher data eksternal yang bergantung pada setting itu diam-diam jatuh ke data placeholder. Jadi di halaman yang sempat lolos render, sebagian data tampil salah tanpa satu pun error yang kelihatan.

Perbaikannya

Perbaikannya dua lapis, dan urutannya penting. Yang pertama menyelesaikan insiden, yang kedua memastikan gejalanya tidak akan seganas ini lagi.

Lapis pertama, langsung: naikkan tier Sanity ke paket berbayar (Growth) supaya kuota tidak lagi jadi tembok. Ini keputusan yang seharusnya diambil sejak hari pertama untuk situs produksi, bukan setelah kejadian. Begitu kuota lega, halaman detail kembali render dan 522 hilang dalam hitungan menit.

Lapis kedua, defensif, supaya kegagalan eksternal tidak pernah lagi menggantung selama 90 detik: pasang timeout eksplisit dan nyalakan useCdn: true.

export const client = createClient({
  projectId,
  dataset,
  apiVersion: "2024-01-01",
  useCdn: true, // konten editorial lewat CDN, bucket kuota terpisah
  timeout: 5000, // milidetik: gagal cepat, jangan menggantung
});

Dua baris ini mengubah karakter kegagalan. useCdn: true menyajikan konten editorial dari CDN Sanity yang punya bucket kuota terpisah dan jauh lebih tahan banting terhadap traffic, jadi tekanan ke API utama turun drastis. timeout: 5000 memastikan kalaupun sesuatu di hulu bermasalah, fetch gagal dalam 5 detik dengan error yang jelas, bukan menggantung sampai Cloudflare memvonis 522. Gagal cepat jauh lebih baik daripada menggantung lama.

Pelajaran

  • Kalau homepage sehat tapi halaman dinamis kena 522, curigai render di origin, bukan jaringan. Homepage yang ter-cache edge menyembunyikan masalah yang cuma muncul saat render menyentuh origin.
  • 522 dari Cloudflare hampir selalu gejala, bukan penyebab. Kejar apa yang menggantung di balik origin, dalam kasus ini 402 dari Sanity.
  • Panggil API eksternal langsung dengan curl sejak awal. Satu perintah curl mengungkap plan_limit_reached yang tidak akan pernah terlihat dari layar 522.
  • Untuk situs produksi, pakai tier berbayar sejak hari pertama, dan jangan biarkan staging berbagi dataset serta kuota yang sama dengan produksi.
  • Setiap fetch ke layanan eksternal wajib punya timeout eksplisit. Fetch tanpa batas mengubah kuota habis menjadi timeout 90 detik.
  • Untuk konten editorial, nyalakan useCdn: true. Bucket kuota terpisah dan lebih tahan lonjakan traffic launch.