D
P
0

Next.js

1,7 Juta API Call Sebulan dengan Nol Pengunjung: TTL Cache 15 Detik Dikali Fragmentation Dikali Bot Crawl

16 Juli 2026·3 menit baca
1,7 Juta API Call Sebulan dengan Nol Pengunjung: TTL Cache 15 Detik Dikali Fragmentation Dikali Bot Crawl

Pesan dari klien masuk, singkat dan bikin dingin: "all api keys for sanity and coingecko have run out." Situs yang saya kerjakan, sebuah situs klien dengan fitur data pasar di atas Next.js 16, saat itu punya nol pengunjung nyata. Belum ada traffic manusia sama sekali. Tapi proyeksi penggunaan CoinGecko menunjuk ke sekitar 1,7 juta call per bulan, jauh di atas plafon 500 ribu di plan Analyst. Sanity lebih parah secara rasio: sekitar 860 ribu write per bulan, padahal batasnya 200 ribu.

Nol pengunjung. 1,7 juta call. Dua angka itu tidak masuk akal berdampingan, dan di situlah cerita ini dimulai.

Tebakan dramatis yang semuanya salah

Refleks pertama saya adalah skenario dramatis. API key bocor dan dipakai orang lain. Ada infinite loop di suatu tempat. Situs kena scraping agresif. Semua hipotesis itu punya satu kesamaan: menyalahkan sesuatu yang luar biasa.

Kenyataannya jauh lebih membosankan. Tidak ada yang bocor, tidak ada yang crash. Setiap baris kode bekerja persis seperti yang ditulis. Yang tidak pernah terjadi cuma satu hal sederhana: tidak ada yang pernah duduk dan menghitung matematikanya.

Akar masalah: perkalian tiga faktor

Di kode fetcher ada konstanta ini:

// feels live without burning quota
const CACHE_TTL_SECONDS = 15;

Komentarnya menjelaskan niatnya: 15 detik supaya data terasa "live" tanpa membakar kuota. Masalahnya, klaim "tanpa membakar kuota" itu tidak pernah diuji dengan aritmetika. Begitu dihitung, ada tiga faktor yang saling mengalikan.

Faktor pertama: TTL 15 detik berarti setiap cache key bisa refetch sampai empat kali per menit. Itu baseline-nya.

Faktor kedua, dan ini yang paling licik: cache fragmentation. Endpoint per-koin dan per-NFT tidak menghasilkan satu cache key. Masing-masing menghasilkan 100+ key, satu untuk setiap resource. TTL melindungi per key, bukan per endpoint. Jadi "satu endpoint dengan cache 15 detik" sebenarnya adalah ratusan timer 15 detik yang berjalan paralel.

Faktor ketiga: nol pengunjung bukan berarti nol traffic. Bot crawler, tab dev yang lupa ditutup dan terus polling, monitoring, regenerasi sitemap, semuanya tetap jalan bahkan sebelum manusia pertama datang. Satu pass bot crawl yang menyentuh N halaman berarti N fresh fetch di setiap jendela TTL.

Kalikan ketiganya, dan angka 1,7 juta per bulan berhenti terlihat misterius.

Kebocoran Sanity jalurnya beda tapi temanya sama. File provider-status.ts menulis 2 request Sanity di setiap invokasi fetcher, dan fetcher itu dipanggil dari 88 call site. Setiap fetch, termasuk jutaan fetch yang dipicu fragmentasi di atas, ikut menulis status. Dari situlah proyeksi 860 ribu write per bulan datang.

Perbaikannya: hitung dulu, baru set angka

TTL untuk endpoint shared naik dari 15 ke 60 detik, dan konstantanya sekarang wajib membawa perhitungannya sendiri:

// CACHE_TTL_SECONDS = 60. Single cache key (shared top-100). Caps at 60x24x30/60s = 43K calls/month max. Analyst limit 500K. OK.
const CACHE_TTL_SECONDS = 60;

Komentar itu bukan dekorasi. Itu format baru yang saya wajibkan: setiap konstanta TTL harus menyebut kardinalitas key-nya dan proyeksi bulanannya. Untuk fetcher per-resource, yang key-nya ratusan, TTL naik ke 300 detik. Total proyeksi turun ke sekitar 250 ribu call per bulan, aman di bawah plafon 500 ribu.

Untuk Sanity, write dari provider-status di-throttle pakai lock Redis di Upstash dengan pola SET NX EX:

const lockKey = `provider-status-lock:${outcome}:${provider}`;
const acquired = await redis.set(lockKey, "1", { nx: true, ex: 300 });
if (!acquired) return; // status ini sudah tercatat dalam 5 menit terakhir

Satu write per kombinasi outcome dan provider per lima menit, tidak peduli seberapa sering fetcher-nya dipanggil. Proyeksinya jatuh dari 860 ribu ke sekitar 14 ribu write per bulan.

Dua aturan permanen lahir dari insiden ini: setiap TTL wajib punya komentar kardinalitas plus proyeksi, dan fetcher per-resource minimal TTL 5 menit.

Pelajaran

  • TTL tanpa proyeksi bulanan bukan konfigurasi, itu tebakan. Rumusnya murah: jumlah key dikali frekuensi refresh dikali durasi sebulan.
  • Cache fragmentation adalah pengali tersembunyi. TTL melindungi per key, jadi hitung berapa key yang sebenarnya dibuat endpoint-mu, bukan berapa endpoint yang kamu punya.
  • Nol pengunjung tidak sama dengan nol traffic. Bot, monitoring, dan tab dev yang polling sudah cukup untuk menjebol kuota sebelum launch.
  • Side effect di jalur fetch ikut terkalikan. Dua write "kecil" per invokasi jadi ratusan ribu ketika invokasinya jutaan.
  • Kuota API berbayar adalah tagihan klien. Hitung sebelum deploy, bukan setelah pesan "keys have run out" masuk.