D
P
0
← Semua artikel Read in English

Next.js & React di Produksi

Kuota Sanity Masih Terbakar 150 Ribu Request per Hari Setelah Cache Dibenerin? 85% Burn-nya Datang dari 4 Singleton `layout`

· · 5 menit baca
Kuota Sanity Masih Terbakar 150 Ribu Request per Hari Setelah Cache Dibenerin? 85% Burn-nya Datang dari 4 Singleton `layout`

Ada satu momen dalam debugging yang rasanya paling tidak enak: kamu sudah memperbaiki penyebab yang paling jelas, dan angkanya tetap tidak bergerak.

Di sebuah situs klien yang jalan di atas Next.js dan Sanity, saya baru saja menambal salah konfigurasi cache yang memang keliru, sebuah route yang dipaksa dinamis padahal tidak perlu sehingga tiap request menembus langsung ke API. Perbaikan itu benar dan tetap perlu, tapi kuotanya tetap terbakar di kisaran 150 ribu request per hari. Tulisan ini tentang apa yang terjadi setelah perbaikan yang "seharusnya cukup" ternyata tidak cukup.

Teori yang paling enak dipercaya

Tersangka favorit semua orang di kasus seperti ini adalah bot. Situsnya punya ribuan halaman detail, sitemap-nya besar, dan Googlebot memang rajin. Narasinya rapi: crawler menyapu ribuan slug, tiap slug memicu beberapa fetch, kuota habis. Saya sempat yakin sekali dengan teori ini. Rencana turunannya bahkan sudah saya susun: batasi crawl rate, tunda indexing sebagian route, bikin robots.txt yang lebih galak.

Untungnya eksekusi rencana itu saya tunda sehari, karena ada satu pertanyaan yang mengganjal. Kalau memang bot, request-nya mestinya datang dari banyak IP dan banyak user agent. Saya tidak punya data apa pun untuk membuktikan atau membantah itu. Yang saya punya cuma angka total di dashboard: besar, dan sama sekali tidak menjelaskan apa-apa.

Nyalakan log delivery dulu, baru berteori

Sanity punya fitur yang jarang disebut orang: pengiriman log request. Di dashboard project ada bagian "Configure log delivery", tinggal dinyalakan dan diarahkan ke bucket tujuan. Setelah aktif, tiap request ke API dan CDN dicatat sebagai satu baris NDJSON lengkap dengan atributnya, termasuk identitas query GROQ yang dipakai.

Saya nyalakan, saya tunggu sampai drop pertama masuk keesokan harinya, lalu saya tarik seluruh berkas yang tersedia. Dari titik itu pekerjaannya berubah dari menebak jadi menghitung. Skrip agregatornya sependek ini:

import fs from "node:fs";
 
const rows = fs.readFileSync("sanity-logs.ndjson", "utf8").split("\n");
const byQuery = new Map();
let total = 0;
 
for (const line of rows) {
  if (!line.trim()) continue;
  const rec = JSON.parse(line);
  const id = rec.attributes?.["sanity.groqQueryIdentifier"] ?? "unknown";
  byQuery.set(id, (byQuery.get(id) ?? 0) + 1);
  total++;
}
 
[...byQuery.entries()]
  .sort((a, b) => b[1] - a[1])
  .slice(0, 10)
  .forEach(([id, n]) =>
    console.log(n.toString().padStart(8), ((n / total) * 100).toFixed(1) + "%", id)
  );

Kuncinya ada di sanity.groqQueryIdentifier. Atribut itu memberi tiap query bentuk identitas yang stabil, jadi ribuan request yang cuma beda parameter tetap terkumpul jadi satu baris ringkasan. Tanpa itu kamu cuma melihat jutaan URL yang mirip-mirip dan tidak bisa menyimpulkan apa-apa.

Angka yang mematahkan teori saya

Totalnya 1,06 juta request selama delapan hari. Hasil agregasinya bikin saya diam sebentar.

Teori bot saya bukan salah total, dia cuma kecil. Kalau saya jalan dengan intuisi, saya akan menghabiskan sehari penuh mengatur crawl budget demi memangkas 14%, sementara 85% penyebab yang sebenarnya tidak tersentuh sama sekali.

Akar masalahnya: singleton yang polos

Empat dokumen itu isinya konfigurasi. Nyaris tidak pernah berubah, mungkin sekali seminggu. Tapi mereka di-fetch oleh layout, header, dan footer, artinya tiap render halaman apa pun menariknya lagi.

Dan di sinilah kesalahan saya yang sesungguhnya: semua getter itu cuma menumpang TTL baseline lima menit. Lima menit terdengar aman sampai kamu kalikan dengan jumlah render. Ribuan halaman dirender ulang sepanjang hari, tiap render memicu empat fetch config, dan tiap jam jendela cache-nya dibuka ulang dua belas kali. Angkanya membesar bukan karena traffic-nya besar, tapi karena perkalian.

Perbaikannya: TTL panjang per singleton, dipasangkan dengan tag

Solusinya bukan menaikkan baseline global. Itu bakal ikut membuat konten yang memang perlu segar jadi basi. Yang saya lakukan adalah menetapkan TTL eksplisit per getter, lalu memasangkannya dengan invalidasi berbasis tag supaya editor tetap melihat perubahannya seketika.

export const getNavConfig = () =>
  sanityFetch<NavConfig>(NAV_CONFIG_QUERY, {}, {
    revalidate: 3600,
    tags: ["navConfig"],
  });

Semua config singleton dapat revalidate: 3600. Satu getter yang menyuplai widget dengan data lebih cepat berubah dapat 1800. Sisanya saya petakan satu per satu sampai jadi matriks 18 getter, masing-masing dengan alasan tertulis kenapa angkanya segitu.

Bagian tag itu yang bikin TTL satu jam tidak menakutkan. Webhook dari CMS memanggil invalidasi berdasarkan tipe dokumen yang barusan diedit:

export async function POST(req: Request) {
  const body = await req.json();
  revalidateTag(body._type);
  return Response.json({ revalidated: true });
}

Jadi TTL panjang mengurus kondisi normal, dan tag mengurus kondisi "editor barusan ganti nomor telepon di footer dan mau lihat hasilnya sekarang juga".

Hasilnya: dari sekitar 150 ribu request per hari turun ke 13 sampai 15 ribu per hari. Sepuluh kali lipat, tanpa menyentuh satu baris pun urusan crawler.

Yang saya bawa pulang