Studi Kasus: Membongkar Kasus API Melambat Tanpa Deployment, Lonjakan Network I/O Database
10 min read

Studi Kasus: Membongkar Kasus API Melambat Tanpa Deployment, Lonjakan Network I/O Database

Hari Jumat, pukul 17.00. Waktu yang seharusnya menjadi penutup minggu kerja. Namun PagerDuty berbunyi. Laporan awal menunjukkan kondisi yang membingungkan — waktu respon API melonjak dari sekitar 200 milidetik menjadi sekitar 8 detik, padahal CPU dan memory di seluruh instance terlihat normal, tidak ada deployment atau perubahan konfigurasi, dan query database tampak berjalan seperti biasa. Masalah dimulai tepat 47 menit yang lalu, dan satu anomali mencolok muncul di dashboard: network I/O pada primary database melonjak tajam di waktu yang sama. Ini bukan insiden biasa — ini kasus klasik yang menuntut pendekatan analitis, bukan asumsi terburu-buru.

Langkah Pertama: Membaca Sinyal, Jangan Menebak

Kesalahan paling umum saat on-call adalah langsung menebak penyebab berdasarkan pengalaman masa lalu — “biasanya kalau lambat begini, pasti query N+1” atau “pasti ada memory leak lagi.” Godaan untuk langsung melompat ke kesimpulan familiar ini wajar, apalagi di bawah tekanan pukul 17.00 hari Jumat. Tapi pada kasus ini, data justru berbicara cukup jelas jika kamu mau berhenti sejenak dan membacanya dengan tenang. Daripada menebak, langkah yang lebih produktif adalah mengeliminasi kemungkinan satu per satu berdasarkan apa yang metrik benar-benar tunjukkan.

flowchart TD
    A[Alert: API lambat 8s] --> B{CPU tinggi?}
    B -- Tidak --> C{Memory tinggi?}
    C -- Tidak --> D{Ada deployment baru?}
    D -- Tidak --> E{Query execution lambat?}
    E -- Tidak --> F[Bottleneck bukan komputasi]
    F --> G[Curigai I/O: Network atau Disk]

Apa yang Tidak Terjadi

Berdasarkan metrik yang masuk, beberapa kemungkinan bisa langsung dicoret. CPU tetap rendah di seluruh instance, sehingga ini bukan CPU-bound problem. Memory stabil tanpa lonjakan, sehingga kemungkinan memory leak atau GC pressure juga gugur. Tidak ada perubahan kode yang masuk dalam beberapa jam terakhir, sehingga regresi deployment bukan penyebabnya. Dan eksekusi query di level database tampak berjalan normal, artinya ini bukan soal query yang lambat secara komputasi.

Empat kemungkinan paling umum sudah tersingkir. Artinya, bottleneck bukan berada di komputasi sama sekali. Jika bukan komputasi, maka yang tersisa — secara logis — adalah I/O.

Proses eliminasi seperti ini jauh lebih reliable dibanding menebak berdasarkan “feeling” atau insiden sebelumnya. Setiap kemungkinan yang gugur mempersempit ruang pencarian secara signifikan, dan mencegah kamu menghabiskan waktu berharga mengejar hipotesis yang sebenarnya sudah terbantahkan oleh data.

Satu Sinyal yang Berteriak: Network I/O Database

Di antara semua metrik yang terlihat normal, lonjakan tajam pada network I/O database adalah petunjuk paling penting — dan paling sering diabaikan oleh engineer yang terbiasa hanya memantau CPU dan memory. Sinyal ini menandakan satu hal yang cukup spesifik: database sedang mengirim data jauh lebih banyak dari biasanya. Bukan membaca lebih lambat. Bukan menghitung lebih berat. Tapi mengirim — dalam volume yang jauh di luar kebiasaan.

Perbedaan ini krusial. Banyak orang menyamakan “database lambat” dengan “query lambat,” padahal keduanya bisa terjadi secara independen. Query bisa selesai dieksekusi dalam hitungan milidetik, tapi jika hasilnya berupa jutaan baris data yang harus ditransfer lewat jaringan, waktu transfer itu sendiri yang menjadi bottleneck — bukan waktu eksekusi.


Diagram Alur Request, Database, dan Network

Untuk memahami masalah ini dengan cepat, perbandingan alur request normal versus saat insiden terjadi akan sangat membantu memvisualisasikan di mana waktu sebenarnya terbuang.

Kondisi Normal

sequenceDiagram
    participant Client
    participant API
    participant DB as Database

    Client->>API: Request
    API->>DB: Query kecil + LIMIT
    DB-->>API: Hasil data kecil
    API-->>Client: Response (~200 ms)

Pada kondisi normal, database mengeksekusi query dengan cepat, data yang dikirim relatif kecil, dan network sama sekali tidak menjadi bottleneck. Seluruh siklus request selesai dalam hitungan ratusan milidetik tanpa hambatan berarti di lapisan manapun.

Kondisi Saat Insiden

sequenceDiagram
    participant Client
    participant API
    participant DB as Database

    Client->>API: Request
    API->>DB: Query tanpa LIMIT / dataset besar
    Note over DB,API: Puluhan-ratusan MB data ditransfer
    Note over API: API idle, menunggu fetch selesai
    DB-->>API: Hasil data (setelah delay panjang)
    API-->>Client: Response (~8 detik)

Pada kondisi ini, eksekusi query di sisi database tetap cepat, namun transfer data memakan waktu yang sangat lama. Thread atau goroutine di sisi API pada dasarnya idle, hanya menunggu network selesai mengirim seluruh hasil query. Diagram inilah yang menjelaskan kontradiksi yang membingungkan tim on-call di awal: CPU rendah, tapi latency tinggi. Keduanya bisa benar secara bersamaan karena CPU mengukur kerja komputasi, sementara latency yang dirasakan user mencakup seluruh waktu tunggu — termasuk waktu transfer data lewat jaringan yang tidak melibatkan CPU sama sekali.


Hipotesis Utama: Query Menghasilkan Dataset Sangat Besar

Berdasarkan seluruh sinyal yang sudah dikumpulkan, hipotesis paling masuk akal adalah ada satu query atau sekumpulan query yang tiba-tiba menghasilkan hasil data dalam jumlah sangat besar, sehingga waktu transfer jaringan mendominasi total response time secara keseluruhan.

Hipotesis ini menjelaskan seluruh gejala secara konsisten. Query bisa selesai dieksekusi dengan cepat, yang menjelaskan mengapa CPU database tetap rendah. Namun aplikasi menunggu lama saat proses fetch rows berlangsung, yang menjelaskan mengapa response time di sisi client melonjak drastis. Dan API terlihat lambat di mata user meskipun database itu sendiri tidak terlihat “sibuk” dari sisi metrik komputasi — karena memang bukan komputasi yang menjadi masalah.

Banyak dashboard monitoring secara default hanya menampilkan query execution time, bukan total waktu fetch data dari database ke aplikasi. Jika dashboard kamu hanya melacak metrik ini, insiden seperti ini bisa terlihat “baik-baik saja” di permukaan, padahal user sudah mengalami timeout di sisi mereka.

Mengapa Masalah Ini Sulit Terdeteksi?

Kesulitan mendeteksi masalah jenis ini berakar dari kebiasaan investigasi yang sudah terlalu standar. Banyak engineer, ketika menghadapi API lambat, secara otomatis memeriksa query execution time, index usage, dan explain plan — tiga hal yang memang relevan untuk masalah komputasi, tapi tidak menangkap dimensi volume data sama sekali.

Padahal kenyataannya sederhana: execution cepat tidak berarti response cepat jika ukuran data yang dihasilkan besar. Contoh paling sederhana untuk menggambarkan ini:

-- ANTI-PATTERN: query tanpa LIMIT pada tabel besar
SELECT * FROM orders WHERE status = 'PAID';

-- BENAR: batasi jumlah baris yang diambil sekaligus
SELECT * FROM orders WHERE status = 'PAID' ORDER BY created_at DESC LIMIT 100;

Query pertama bisa saja dieksekusi sangat cepat oleh database engine — index pada kolom status membuat pencarian baris yang cocok hampir instan. Tapi jika kondisi status = 'PAID' cocok dengan ratusan ribu baris, waktu untuk mentransfer seluruh baris itu dari database ke aplikasi bisa mencapai beberapa detik, terlepas dari seberapa cepat query itu sendiri “selesai” di sisi database.


Petunjuk Waktu: Mengapa Tepat 47 Menit Lalu?

Detail waktu sering diabaikan, padahal sering menyimpan petunjuk paling berharga. Masalah yang muncul tepat di menit tertentu — bukan secara gradual, tapi seperti tiba-tiba “menyala” — hampir selalu disebabkan oleh sistem terjadwal, bukan oleh aktivitas manusia yang sifatnya acak.

Beberapa kemungkinan yang patut dicurigai di titik ini meliputi cron job internal yang berjalan pada interval tertentu, scheduled report atau proses export data yang dipicu otomatis, cache TTL yang habis secara serentak untuk sejumlah besar key sekaligus, atau background worker yang aktif secara berkala dan kebetulan menyentuh tabel besar.

Tidak ada deploy yang tercatat, tapi perilaku sistem berubah secara tiba-tiba — dan perubahan tiba-tiba semacam ini, tanpa adanya perubahan kode, adalah tanda tangan khas dari sesuatu yang terjadwal dan berjalan otomatis di latar belakang.


Pola Umum Penyebab di Lapangan

Dari pengalaman investigasi insiden serupa di banyak sistem produksi, beberapa pola penyebab muncul berulang kali dengan variasi yang relatif kecil.

Pagination atau LIMIT yang Hilang

Salah satu pola paling sering ditemukan adalah perubahan kecil yang tidak disadari pada query yang sebelumnya sudah dibatasi dengan benar.

-- BENAR: query dengan LIMIT eksplisit
SELECT * FROM users ORDER BY created_at DESC LIMIT 50;

-- ANTI-PATTERN: LIMIT yang tanpa sengaja terhapus atau terlewat
SELECT * FROM users ORDER BY created_at DESC;

Perubahan sekecil menghapus klausa LIMIT — entah karena refactor yang tidak disengaja, kondisi yang membuat parameter limit tidak terisi, atau bug di lapisan query builder — bisa mengubah dataset yang dikembalikan dari puluhan baris menjadi ratusan ribu baris. Dampaknya bukan pada CPU, melainkan langsung pada network saturation, persis seperti yang terlihat pada insiden ini.

Cache Miss Storm

Pola kedua yang sering muncul adalah ketika cache untuk sejumlah besar key kedaluwarsa pada waktu yang hampir bersamaan. Begitu itu terjadi, seluruh request yang sebelumnya dilayani oleh cache tiba-tiba menembus langsung ke database, dan query yang sama — yang masing-masing bisa saja mengirim data dalam jumlah besar — dieksekusi berulang kali dalam rentang waktu singkat. Network I/O database naik drastis sebagai akibatnya, meskipun setiap query individual sebenarnya berjalan dengan cepat.

Scheduled Export atau Integrasi Eksternal

Pola ketiga melibatkan pihak eksternal terhadap sistem inti — seperti BI tool yang menarik data secara berkala, proses data sync dengan partner, atau fitur admin untuk export CSV. Proses semacam ini sering punya tiga karakteristik yang membuatnya licin untuk terdeteksi: tidak tercatat sebagai deployment karena memang bukan perubahan kode, mengakses database secara langsung tanpa melalui API utama, dan menarik data dalam jumlah besar sebagai bagian normal dari fungsinya.


Langkah Investigasi Teknis yang Tepat

Setelah hipotesis cukup kuat, langkah berikutnya adalah memverifikasi dengan data konkret, bukan berhenti di level spekulasi.

Cari Query dengan Jumlah Rows Terbesar

Fokus investigasi di tahap ini bukan pada durasi eksekusi, melainkan pada jumlah baris yang dikembalikan. Pada PostgreSQL, ekstensi pg_stat_statements menyediakan visibility ini secara langsung.

SELECT query, calls, rows
FROM pg_stat_statements
ORDER BY rows DESC
LIMIT 10;

Bandingkan hasil ini dengan baseline historis dari hari-hari sebelumnya di jam yang sama. Query yang biasanya mengembalikan ratusan baris tapi tiba-tiba mengembalikan ratusan ribu baris adalah kandidat utama penyebab insiden.

Pecah Waktu di APM atau Tracing

Pada tool tracing seperti APM, perhatikan baik-baik perbedaan antara dua metrik yang sering tercampur jadi satu angka: query execution time dan fetch atau transfer time. Pola khas yang muncul pada insiden seperti ini biasanya terlihat jelas begitu dipisah:

Execution: 40 ms
Fetch / Network: 7.8 s

Pemisahan ini menjadi konfirmasi konkret bahwa bottleneck memang berada di transfer data, bukan di proses query itu sendiri — selaras dengan seluruh hipotesis yang sudah dibangun dari awal.

Audit Job Terjadwal

Sejalan dengan petunjuk waktu yang dibahas sebelumnya, langkah audit yang relevan adalah mencari proses yang aktif tepat di menit yang sama dengan munculnya insiden, mengakses tabel-tabel besar dalam sistem, dan tidak melalui jalur API utama — karakteristik yang biasanya menunjuk pada cron job, scheduler, atau proses background yang berjalan independen dari traffic user biasa.

flowchart LR
    A[pg_stat_statements: cari rows terbesar] --> B[APM: pisahkan execution vs fetch time]
    B --> C[Audit cron/scheduler aktif di waktu insiden]
    C --> D{Penyebab ditemukan?}
    D -- Ya --> E[Konfirmasi root cause]
    D -- Tidak --> A

Mitigasi Cepat Saat Insiden

Ketika insiden masih berlangsung dan tim sedang dalam mode pemadaman api, prioritas utama bukan mencari solusi sempurna — melainkan menstabilkan sistem secepat mungkin agar dampak ke user berkurang.

TINDAKAN MITIGASI SEGERA:
  □ Terapkan hard limit pada hasil query yang dicurigai
  □ Batasi ukuran response API untuk endpoint yang terdampak
  □ Hentikan sementara job export yang dicurigai sebagai pemicu
  □ Tambahkan rate limit pada endpoint yang sensitif terhadap lonjakan data

Tujuan dari langkah-langkah ini bukan menyelesaikan root cause secara permanen, melainkan menghentikan pendarahan — mengembalikan API ke kondisi yang bisa melayani user secara wajar, sambil investigasi akar masalah berlanjut tanpa tekanan waktu yang sama mendesaknya.

Hindari godaan untuk langsung melakukan restart instance atau database tanpa memahami root cause terlebih dahulu. Jika penyebabnya adalah job terjadwal yang masih berjalan, restart hanya akan menunda masalah beberapa menit sebelum kembali muncul — sementara kamu kehilangan kesempatan untuk menangkap kondisi sistem yang sedang bermasalah untuk investigasi lebih lanjut.

Pelajaran Arsitektural

Insiden ini, di luar resolusinya, mengajarkan beberapa hal penting tentang bagaimana sistem produksi seharusnya dipantau. Monitoring tidak boleh hanya berfokus pada CPU dan memory — dua metrik yang paling umum dipasang di dashboard, tapi sama sekali tidak menangkap dimensi volume data yang mengalir antar layer sistem. Ukuran data dan network I/O harus diperlakukan sebagai first-class metric, setara pentingnya dengan metrik komputasi yang lebih konvensional. Dan yang paling penting, query yang “cepat” secara eksekusi tetap bisa melumpuhkan sistem secara keseluruhan, jika volume data yang dihasilkannya jauh melampaui apa yang pernah dipertimbangkan ketika query itu pertama kali ditulis.


Ringkasan

  • Eliminasi sistematis lebih reliable daripada menebak — coret kemungkinan satu per satu berdasarkan metrik nyata (CPU, memory, deployment, query execution), bukan insting dari insiden sebelumnya.
  • CPU rendah + latency tinggi mengarah ke I/O, bukan komputasi — periksa network I/O dan ukuran data yang ditransfer, bukan hanya waktu eksekusi query.
  • Execution time dan fetch/transfer time adalah dua metrik berbeda — query bisa “cepat” secara eksekusi tapi tetap melumpuhkan sistem jika hasilnya berupa dataset sangat besar.
  • Masalah yang muncul di waktu yang sangat spesifik biasanya berasal dari sistem terjadwal (cron, scheduled export, cache TTL serentak), bukan aktivitas manusia.
  • Tiga pola umum penyebab: LIMIT/pagination yang hilang dari query, cache miss storm yang membuat banyak request menembus database bersamaan, dan scheduled export atau integrasi eksternal yang menarik data besar tanpa melalui API utama.
  • pg_stat_statements dan APM tracing adalah alat investigasi kunci untuk memisahkan masalah volume data dari masalah komputasi.
  • Saat insiden masih berlangsung, prioritaskan mitigasi cepat (hard limit, rate limit, hentikan job yang dicurigai) di atas pencarian solusi sempurna.
  • Monitoring CPU dan memory saja tidak cukup — ukuran data dan network I/O harus jadi metrik first-class dalam observability sistem produksi.

Portofolio