D
P
0
← Semua artikel Read in English

Next.js & React di Produksi

Komponen Countdown Nembak API Pihak Ketiga Langsung dari Browser Tiap 60 Detik: Satu Tab 8 Jam Sama dengan 480 Panggilan

· · 5 menit baca
Komponen Countdown Nembak API Pihak Ketiga Langsung dari Browser Tiap 60 Detik: Satu Tab 8 Jam Sama dengan 480 Panggilan

Audit-audit sebelumnya di proyek ini selalu berakhir dengan kata "aman", dan saya berhenti mempercayai kesimpulan itu. Bukan karena saya punya bukti sebaliknya waktu itu, tapi karena yang diperiksa cuma hal-hal yang gampang dicentang, sementara pagi itu klien mengabari lewat WhatsApp bahwa semua API key untuk CMS dan penyedia data harganya sudah habis. Pemeriksaan yang berhenti di permukaan jelas tidak akan pernah sampai ke penyebab seperti itu, jadi saya minta audit business-logic yang sungguhan.

Auditnya menyapu delapan kategori dan melewati seluruh lapisan klien API, semua route handler, area admin, setiap page.tsx, dan setiap komponen yang melakukan fetch. Tiga temuan naik ke tingkat CRITICAL dan sudah diperbaiki lebih dulu di hari yang sama: TTL 15 detik di penyedia data harga yang proyeksinya 1,7 juta panggilan per bulan melawan plafon 500 ribu, dan pencatat status provider yang menulis di setiap panggilan fetcher sehingga proyeksinya 860 ribu tulisan CMS per bulan melawan batas 200 ribu.

Tapi yang paling lama menempel di kepala saya justru temuan yang cuma dapat peringkat MEDIUM.

Komponen yang menelepon ke luar sendiri

Ada komponen hitung mundur di halaman yang butuh tinggi blok terbaru dari sebuah API blockchain publik. Cara dia mengambil angka itu kira-kira begini bentuknya:

// Dari browser setiap pengunjung, setiap 60 detik, tanpa cache
useEffect(() => {
  const load = () =>
    fetch("https://public-chain-api.example/blocks/tip/height", {
      cache: "no-store",
    })
      .then((r) => r.json())
      .then(setHeight);
 
  load();
  const id = setInterval(load, 60_000);
  return () => clearInterval(id);
}, []);

Tidak ada yang rusak di sana. Komponennya menampilkan angka yang benar, dan di layar saya waktu development dia terasa ringan karena satu tab memang cuma satu panggilan per menit.

Akar masalahnya bukan di kualitas kodenya, tapi di kabelnya: polling sisi klien disambungkan langsung ke upstream, bukan lewat cache di server. Dan begitu sebuah tab terbuka, dia berhenti peduli apakah ada orang yang sedang melihatnya. Tab yang ditinggal terbuka tetap polling 24 jam sehari.

Aritmetika satu tab

Angka pertama yang bikin saya berhenti membaca kode dan mulai menghitung: satu tab yang dibiarkan terbuka 8 jam menghasilkan sekitar 480 polling. Turunannya sederhana, 8 jam sama dengan 480 menit, satu polling per menit, jadi 480 panggilan dari satu orang yang mungkin sudah pergi makan siang sejak jam kedua.

Komponen countdown juga bukan satu-satunya yang polling. Ada ticker harga berjalan yang menembak endpoint internal setiap 15 detik, artinya empat kali per menit, atau sekitar 1.920 polling di tab 8 jam yang sama. Dijumlahkan, satu tab itu mengirim sekitar 2.400 permintaan. Tab yang menyala berhari-hari berarti ribuan polling per browser.

Lalu kalikan dengan jumlah pengunjung. Angka yang dicatat audit untuk komponen countdown: 100 pengunjung serentak dikali satu panggilan per menit sama dengan 100 panggilan per menit ke API pihak ketiga gratis. Yang penting di kalimat itu bukan angkanya, tapi ke mana panggilannya pergi. Bukan ke server saya, melainkan ke server orang lain, dari seratus alamat IP yang berbeda, tanpa satu pun lapisan di tengah yang bisa menahan atau menggabungkannya.

Spec auditnya menaruh konsekuensinya sebagai dugaan, bukan sebagai kejadian yang sudah tercatat: rate-limit atau IP ban yang membuat komponennya rusak tanpa suara. Saya tidak punya bukti bahwa ban itu pernah terjadi. Yang saya punya cuma bentuk pemakaiannya, dan bentuk itu sudah cukup untuk memperbaikinya.

Dua kebocoran yang bentuknya sama, tagihannya beda

Ticker jalurnya berbeda karena dia menembak endpoint internal, bukan API luar. Tapi endpoint itu memakai Cache-Control: no-store, jadi tidak ada satu pun lapisan yang boleh menyimpan responsnya. Dengan polling 15 detik dari setiap pengunjung, 100 pengunjung serentak berarti sekitar 400 request origin per menit hanya untuk ticker.

Bagian yang menarik, di ticker masalahnya bukan kuota penyedia data harga. Cache Redis sudah menyerap kuota itu dengan baik. Yang tidak perlu justru beban komputasi di origin: setiap satu dari 400 request per menit itu membangunkan fungsi server untuk menyusun ulang respons yang isinya sama persis dengan yang baru saja dia kembalikan beberapa detik sebelumnya.

Perbaikan: satu panggilan bersama, bukan satu per pengunjung

Untuk countdown, perbaikannya berupa endpoint server baru yang mem-proxy API pihak ketiga itu, menyimpan hasilnya di Redis dengan TTL 5 menit, lalu ditambah edge cache s-maxage=300. Komponennya diarahkan ke endpoint internal itu, bukan lagi ke domain luar.

// app/api/chain-height/route.ts
export async function GET() {
  let height = await redis.get(KEY);
 
  if (!height) {
    height = await fetchTipHeight();
    await redis.set(KEY, height, { ex: 300 });
  }
 
  return Response.json(
    { height },
    { headers: { "Cache-Control": "public, s-maxage=300" } },
  );
}

Setelah itu, jumlah panggilan ke upstream jadi satu panggilan bersama per 5 menit tanpa peduli berapa jumlah pengunjungnya. Batas atasnya gampang diperiksa sendiri: 5 menit berarti 12 panggilan per jam, 288 per hari, sama saja mau yang membuka situsnya satu orang atau seribu.

Untuk ticker, perbaikannya cuma mengganti satu header:

Cache-Control: public, max-age=10, s-maxage=10, stale-while-revalidate=30

Sekarang CDN yang menyerap polling-nya, bukan origin. Harga yang dibayar adalah kesegaran: basi paling buruk 70 detik, dari 10 detik di edge ditambah 60 detik dari lapisan di bawahnya. Untuk teks harga yang berjalan di atas halaman, jeda selebar itu tidak kelihatan.

Tidak semua temuan di tingkat yang sama ikut saya perbaiki. Ada satu lagi, penulis audit log yang menulis ke CMS di setiap Server Action admin, pola anti yang persis sama dengan pencatat status provider tadi. Yang ini sengaja diterima apa adanya tanpa perbaikan, karena aksinya terkunci autentikasi dan volumenya rendah.

Kesimpulan auditnya sendiri cukup melegakan: aplikasinya pada dasarnya sehat, dan tidak ada lagi kebocoran kuota diam-diam yang tersisa.

Pelajaran