Playbook tuning performa Go + Postgres untuk API yang dihasilkan AI: atur pool koneksi, baca rencana kueri, indeks cerdas, paginasi aman, dan bentuk JSON lebih cepat.

API yang dihasilkan AI bisa terasa cepat saat pengujian awal. Anda memanggil endpoint beberapa kali, dataset kecil, dan permintaan datang satu per satu. Lalu lalu lintas nyata muncul: endpoint campuran, beban bursty, cache lebih dingin, dan lebih banyak baris dari yang diperkirakan. Kode yang sama bisa mulai terasa lambat secara acak meski tidak ada yang rusak.
Lambat biasanya muncul dalam beberapa cara: lonjakan latensi (kebanyakan permintaan baik, beberapa butuh 5x hingga 50x lebih lama), timeouts (persen kecil gagal), atau CPU panas (CPU Postgres karena kerja kueri, atau CPU Go karena JSON, goroutine, logging, dan retry).
Skenario umum adalah endpoint daftar dengan filter fleksibel yang mengembalikan respons JSON besar. Di database uji, ia memindai beberapa ribu baris dan selesai cepat. Di produksi, ia memindai beberapa juta baris, mengurutkannya, dan baru kemudian menerapkan LIMIT. API masih "bekerja", tapi p95 melambung dan beberapa permintaan time out saat burst.
Untuk memisahkan kelambanan database dari kelambanan aplikasi, pertahankan model mental sederhana.
Jika database yang lambat, handler Go Anda menghabiskan sebagian besar waktunya menunggu kueri. Anda mungkin juga melihat banyak permintaan "in flight" sementara CPU Go terlihat normal.
Jika aplikasi yang lambat, kueri selesai cepat, tapi waktu hilang setelah kueri: membangun objek respons besar, marshaling JSON, menjalankan kueri ekstra per baris, atau melakukan terlalu banyak kerja per permintaan. CPU dan memori Go naik, dan latensi tumbuh seiring ukuran respons.
"Cukup baik" sebelum peluncuran bukan berarti sempurna. Untuk banyak endpoint CRUD, targetkan p95 latency yang stabil (bukan hanya rata-rata), perilaku yang dapat diprediksi saat burst, dan tidak ada timeouts di puncak yang Anda harapkan. Tujuannya sederhana: tidak ada permintaan lambat mengejutkan ketika data dan lalu lintas tumbuh, dan sinyal yang jelas saat sesuatu bergeser.
Sebelum Anda menyetel apa pun, tentukan apa arti "baik" untuk API Anda. Tanpa baseline, mudah menghabiskan jam mengubah pengaturan dan tetap tidak tahu apakah Anda memperbaiki atau hanya memindahkan bottleneck.
Tiga angka biasanya menceritakan sebagian besar:
p95 adalah metrik "hari buruk". Jika p95 tinggi tapi rata-rata baik, sekelompok kecil permintaan melakukan terlalu banyak kerja, terblokir pada lock, atau memicu rencana lambat.
Buat kueri lambat terlihat sejak awal. Di Postgres, aktifkan slow query logging dengan ambang rendah untuk pengujian pra-launch (misalnya 100–200 ms), dan catat pernyataan lengkap sehingga bisa Anda salin ke klien SQL. Jangan lama-lama; mencatat semua slow query di produksi cepat jadi bising.
Selanjutnya, uji dengan permintaan yang mirip nyata, bukan hanya route "hello world" tunggal. Sekelompok kecil cukup jika cocok dengan apa yang pengguna akan lakukan: panggilan daftar dengan filter dan sorting, halaman detail dengan beberapa join, create atau update dengan validasi, dan kueri ala search dengan kecocokan parsial.
Jika Anda menghasilkan endpoint dari spesifikasi (mis. dengan alat vibe-coding seperti Koder.ai), jalankan segelintir request yang sama berulang dengan input konsisten. Itu memudahkan mengukur perubahan seperti indeks, tweak paginasi, dan penulisan ulang kueri.
Terakhir, pilih target yang bisa Anda sebutkan dengan jelas. Contoh: "Sebagian besar permintaan tetap di bawah 200 ms p95 pada 50 pengguna konkuren, dan error tetap di bawah 0.5%." Angka tepat bergantung pada produk Anda, tapi target jelas mencegah ngoprek tanpa akhir.
Connection pool menjaga jumlah koneksi database yang terbuka terbatas dan menggunakan kembali koneksi tersebut. Tanpa pool, tiap permintaan bisa membuka koneksi baru, dan Postgres membuang waktu dan memori mengelola session alih-alih menjalankan kueri.
Tujuannya membuat Postgres sibuk melakukan kerja berguna, bukan melakukan context-switching antar terlalu banyak koneksi. Ini sering jadi kemenangan berarti pertama, terutama untuk API yang dihasilkan AI yang bisa diam-diam berubah jadi endpoint yang banyak bicara.
Di Go, biasanya Anda menyetel max open connections, max idle connections, dan lifetime koneksi. Titik awal aman untuk banyak API kecil adalah kelipatan kecil dari core CPU Anda (sering 5 hingga 20 total koneksi), dengan jumlah idle serupa, dan daur ulang koneksi secara berkala (misalnya setiap 30–60 menit).
Jika Anda menjalankan beberapa instance API, ingat pool berlipat. Pool 20 koneksi di 10 instance berarti 200 koneksi ke Postgres—cara tim tiba-tiba menemui batas koneksi.
Masalah pool terasa berbeda dari SQL lambat.
Jika pool terlalu kecil, permintaan menunggu sebelum sampai ke Postgres. Latensi melonjak, tapi CPU database dan waktu kueri mungkin terlihat baik.
Jika pool terlalu besar, Postgres tampak kelebihan beban: banyak sesi aktif, tekanan memori, dan latensi tak merata antar endpoint.
Cara cepat memisahkan keduanya adalah mengukur panggilan DB dalam dua bagian: waktu menunggu koneksi vs waktu mengeksekusi kueri. Jika sebagian besar waktu adalah "menunggu", pool adalah bottleneck. Jika sebagian besar waktu adalah "di kueri", fokus pada SQL dan indeks.
Pemeriksaan cepat yang berguna:
max_connections.Jika Anda menggunakan pgxpool, Anda mendapatkan pool berfokus pada Postgres dengan statistik jelas dan default yang baik untuk perilaku Postgres. Jika Anda menggunakan database/sql, Anda mendapatkan interface standar yang bekerja lintas database, tapi Anda harus eksplisit tentang pengaturan pool dan perilaku driver.
Aturan praktis: jika Anda total pada Postgres dan ingin kontrol langsung, pgxpool sering lebih sederhana. Jika Anda bergantung pada library yang mengharapkan database/sql, bertahanlah dengan itu, tetapkan pool secara eksplisit, dan ukur waktu tunggu.
Contoh: sebuah endpoint yang menampilkan daftar order mungkin berjalan 20 ms, tapi saat 100 pengguna konkuren naik menjadi 2 s. Jika log menunjukkan 1.9 s menunggu koneksi, tuning kueri tidak akan membantu sampai pool dan total koneksi Postgres disesuaikan.
Saat sebuah endpoint terasa lambat, periksa apa yang sebenarnya dilakukan Postgres. Membaca cepat EXPLAIN sering menunjuk perbaikan dalam hitungan menit.
Jalankan ini pada SQL persis yang dikirim API:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Beberapa baris yang penting. Lihat node teratas (apa yang dipilih Postgres) dan total di bagian bawah (berapa lama). Lalu bandingkan estimasi vs baris aktual. Gap besar biasanya berarti planner salah tebak.
Jika Anda melihat Index Scan atau Index Only Scan, Postgres menggunakan indeks, yang biasanya baik. Bitmap Heap Scan bisa ok untuk kecocokan ukuran menengah. Seq Scan berarti membaca seluruh tabel, yang hanya ok ketika tabel kecil atau hampir semua baris cocok.
Bendera merah umum:
ORDER BY)Rencana lambat biasanya datang dari beberapa pola:
WHERE + ORDER BY Anda (mis. (user_id, status, created_at))WHERE (mis. WHERE lower(email) = $1), yang bisa memaksa scan kecuali Anda menambah indeks ekspresi yang sesuaiJika rencana terlihat aneh dan estimasi jauh meleset, statistik seringkali usang. Jalankan ANALYZE (atau biarkan autovacuum mengejar) supaya Postgres tahu jumlah baris dan distribusi nilai saat ini. Ini penting setelah import besar atau ketika endpoint baru mulai menulis banyak data dengan cepat.
Indeks hanya membantu ketika cocok dengan cara API Anda melakukan query. Jika Anda membuatnya dari tebakan, Anda mendapat tulis yang lebih lambat, penyimpanan lebih besar, dan sedikit percepatan.
Cara praktis memikirkan: indeks adalah jalan pintas untuk pertanyaan spesifik. Jika API Anda menanyakan hal berbeda, Postgres mengabaikan jalan pintas itu.
Jika endpoint memfilter account_id dan mengurutkan created_at DESC, sebuah indeks komposit sering lebih baik daripada dua indeks terpisah. Itu membantu Postgres menemukan baris yang tepat dan mengembalikannya dalam urutan yang benar dengan kerja lebih sedikit.
Aturan praktis yang biasanya berlaku:
Contoh: jika API Anda GET /orders?status=paid dan selalu tampilkan terbaru, indeks seperti (status, created_at DESC) cocok. Jika kebanyakan kueri juga memfilter berdasarkan customer, (customer_id, status, created_at) bisa lebih baik, tapi hanya jika itu memang cara endpoint berjalan di produksi.
Jika sebagian besar lalu lintas mengenai irisan sempit baris, partial index bisa lebih murah dan lebih cepat. Misalnya, jika aplikasi Anda kebanyakan membaca record aktif, mengindeks hanya WHERE active = true menjaga indeks lebih kecil dan lebih mungkin tetap di memori.
Untuk memastikan indeks membantu, lakukan pengecekan cepat:
EXPLAIN (atau EXPLAIN ANALYZE di lingkungan aman) dan cari index scan yang cocok dengan query Anda.Hapus indeks yang tak terpakai dengan hati-hati. Periksa statistik penggunaan (mis. apakah indeks pernah discan). Hapus satu per satu di jendela risiko rendah, dan siapkan rencana rollback. Indeks yang tak dipakai tidak tanpa efek; mereka memperlambat insert dan update pada tiap tulis.
Paginasi sering jadi titik di mana API cepat mulai terasa lambat, walau database sehat. Perlakukan paginasi sebagai masalah desain kueri, bukan detail UI.
LIMIT/OFFSET terlihat sederhana, tapi halaman dalam biasanya lebih mahal. Postgres masih harus melewati (dan sering mengurutkan) baris yang Anda lewatkan. Halaman 1 mungkin menyentuh beberapa puluh baris. Halaman 500 bisa memaksa database memindai dan membuang puluhan ribu baris hanya untuk mengembalikan 20 hasil.
Ini juga bisa menghasilkan hasil yang tidak stabil ketika baris disisipkan atau dihapus antar permintaan. Pengguna mungkin melihat duplikat atau kehilangan item karena arti "baris 10.000" berubah saat tabel berubah.
Keyset pagination menanyakan pertanyaan berbeda: "Berikan 20 baris berikutnya setelah baris terakhir yang saya lihat." Itu membuat database bekerja pada irisan kecil dan konsisten.
Versi sederhana menggunakan id yang meningkat:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
API Anda mengembalikan next_cursor sama dengan id terakhir di halaman. Permintaan berikutnya menggunakan nilai itu sebagai $1.
Untuk pengurutan berbasis waktu, gunakan urutan stabil dan pemecah seri. created_at saja tidak cukup jika dua baris punya timestamp sama. Gunakan cursor komposit:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Beberapa aturan untuk mencegah duplikat dan item hilang:
ORDER BY (biasanya id).created_at dan id bersama).Alasan yang sering mengejutkan bahwa API terasa lambat bukanlah database. Itu adalah respons. JSON besar butuh waktu lebih lama untuk dibangun, dikirim, dan di-parse klien. Kemenangan tercepat sering adalah mengembalikan lebih sedikit.
Mulai dari SELECT Anda. Jika endpoint hanya butuh id, name, dan status, minta kolom tersebut saja. SELECT * diam-diam menjadi lebih berat seiring tabel bertambah text panjang, blob JSON, dan kolom audit.
Lambat lain yang sering muncul adalah pola N+1 saat membangun respons: Anda mengambil daftar 50 item, lalu menjalankan 50 kueri lagi untuk melampirkan data terkait. Itu mungkin lolos pengujian, lalu runtuh di lalu lintas nyata. Lebih baik satu kueri yang mengembalikan apa yang Anda butuhkan (join hati-hati), atau dua kueri di mana yang kedua melakukan batch berdasarkan ID.
Beberapa cara menjaga payload kecil tanpa merusak klien:
include= (atau mask fields=) sehingga respons daftar tetap ramping dan detail mengaktifkan tambahan.Keduanya bisa cepat. Pilih berdasarkan apa yang Anda optimalkan.
Fungsi JSON Postgres (jsonb_build_object, json_agg) berguna ketika Anda ingin lebih sedikit round trip dan bentuk yang terduga dari satu kueri. Membentuk di Go berguna ketika Anda butuh logika kondisional, reuse struct, atau menjaga SQL lebih mudah dibaca. Jika SQL untuk membentuk JSON menjadi sulit dibaca, ia juga sulit untuk dituning.
Aturan bagus: biarkan Postgres memfilter, mengurutkan, dan mengagregasi. Biarkan Go menangani presentasi akhir.
Jika Anda menghasilkan API cepat (mis. dengan Koder.ai), menambahkan flag include lebih awal membantu menghindari endpoint yang membengkak seiring waktu. Itu juga memberi cara aman menambah field tanpa membuat setiap respons lebih berat.
Anda tidak perlu lab tes besar untuk menangkap sebagian besar masalah performa. Sebuah pass singkat dan dapat diulang memunculkan masalah yang berubah menjadi outage saat lalu lintas muncul, terutama ketika titik awalnya adalah kode yang dihasilkan yang akan Anda kirim.
Sebelum mengubah apa pun, tulis baseline kecil:
Mulai kecil, ubah satu hal pada satu waktu, dan uji ulang setelah tiap perubahan.
Jalankan load test 10–15 menit yang menyerupai penggunaan nyata. Hantam endpoint yang akan diakses pengguna pertama (login, daftar, search, create). Lalu urutkan route berdasarkan p95 latency dan total waktu.
Cek tekanan koneksi sebelum tuning SQL. Pool yang terlalu besar membanjiri Postgres. Pool yang terlalu kecil menciptakan waktu tunggu panjang. Cari waktu tunggu untuk mengambil koneksi yang meningkat dan jumlah koneksi yang spike saat burst. Sesuaikan pool dan idle limits dulu, lalu uji ulang.
EXPLAIN kueri paling lambat dan perbaiki red flag terbesar. Pelakunya biasanya full table scan pada tabel besar, sort pada result set besar, dan join yang meledakkan jumlah baris. Pilih satu kueri terburuk dan buat itu membosankan (boring).
Tambah atau sesuaikan satu indeks, lalu uji ulang. Indeks membantu ketika cocok dengan WHERE dan ORDER BY Anda. Jangan tambah lima sekaligus. Jika endpoint lambat adalah "list orders by user_id ordered by created_at", indeks komposit (user_id, created_at) bisa jadi perbedaan antara instan dan menyakitkan.
Perkecil respons dan perbaiki paginasi, lalu uji lagi. Jika endpoint mengembalikan 50 baris dengan JSON besar, database, jaringan, dan klien semuanya terbebani. Kembalikan hanya field yang UI butuhkan, dan pilih paginasi yang tidak melambat seiring tabel tumbuh.
Simpan log perubahan sederhana: apa yang diubah, kenapa, dan apa yang bergerak di p95. Jika perubahan tidak memperbaiki baseline, revert dan lanjut.
Sebagian besar masalah performa pada API Go + Postgres adalah yang dibuat sendiri. Kabar baik: beberapa pemeriksaan menangkap banyak masalah sebelum lalu lintas nyata datang.
Salah satu jebakan klasik adalah memperlakukan ukuran pool seperti kenop kecepatan. Menetapkannya "setinggi mungkin" sering membuat semuanya lebih lambat. Postgres menghabiskan lebih banyak waktu mengatur sesi, memori, dan lock, dan aplikasi Anda mulai time out bergelombang. Pool kecil dan stabil dengan concurrency yang dapat diprediksi biasanya menang.
Kesalahan umum lain adalah "mengindeks semuanya." Indeks tambahan bisa membantu baca, tapi juga memperlambat tulis dan dapat mengubah rencana kueri dengan cara mengejutkan. Jika API Anda sering insert atau update, tiap indeks tambahan menambah kerja. Ukur sebelum dan sesudah, dan periksa rencana setelah menambah indeks.
Utang paginasi merayap diam-diam. Offset pagination terlihat baik awalnya, lalu p95 naik karena database harus melewati semakin banyak baris.
Ukuran payload JSON adalah pajak tersembunyi lainnya. Kompresi membantu bandwidth, tapi tidak menghapus biaya membangun, mengalokasi, dan mengurai objek besar. Pangkas field, hindari nesting dalam, dan kembalikan hanya yang layar butuhkan.
Jika Anda hanya memantau rata-rata response time, Anda akan melewatkan titik sakit pengguna nyata. p95 (dan kadang p99) adalah tempat saturasi pool, lock wait, dan rencana lambat muncul pertama kali.
Cek cepat pra-launch:
EXPLAIN lagi setelah menambah indeks atau mengubah filter.Sebelum pengguna nyata datang, Anda ingin bukti API Anda tetap dapat diprediksi di bawah tekanan. Tujuannya bukan angka sempurna, melainkan menangkap beberapa isu yang menyebabkan timeouts, lonjakan, atau database yang berhenti menerima kerja baru.
Jalankan pemeriksaan di staging yang mirip produksi (ukuran DB serupa, indeks sama, pengaturan pool sama): ukur p95 latency per endpoint kunci di bawah beban, tangkap kueri terlambat teratas berdasarkan total waktu, pantau waktu tunggu pool, lakukan EXPLAIN (ANALYZE, BUFFERS) pada kueri terburuk untuk memastikan ia menggunakan indeks yang Anda harapkan, dan periksa ukuran payload pada route tersibuk.
Lalu lakukan satu uji worst-case yang meniru bagaimana produk bisa rusak: minta halaman dalam yang dalam, terapkan filter paling luas, dan coba dengan cold start (restart API dan hantam permintaan yang sama pertama kali). Jika paginasi dalam semakin lambat tiap halaman, beralihlah ke cursor-based pagination sebelum peluncuran.
Tuliskan default Anda supaya tim membuat pilihan konsisten nanti: batas dan timeout pool, aturan paginasi (max page size, apakah offset diperbolehkan, format cursor), aturan kueri (pilih hanya kolom yang diperlukan, hindari SELECT *, batasi filter mahal), dan aturan logging (ambang slow query, berapa lama menyimpan sampel, cara memberi label endpoint).
Jika Anda membangun dan mengekspor layanan Go + Postgres dengan Koder.ai, melakukan pass perencanaan singkat sebelum deployment membantu menjaga filter, paginasi, dan bentuk respons tetap disengaja. Setelah Anda mulai menyetel indeks dan bentuk kueri, snapshot dan rollback memudahkan mengembalikan "perbaikan" yang membantu satu endpoint tapi merugikan yang lain. Jika Anda ingin satu tempat untuk mengiterasi workflow itu, Koder.ai di koder.ai dirancang untuk menghasilkan dan menyempurnakan layanan tersebut lewat chat, lalu mengekspor source ketika siap.
Mulailah dengan memisahkan waktu tunggu DB dari waktu kerja aplikasi.
Tambahkan pengukuran sederhana untuk “menunggu koneksi” dan “eksekusi kueri” untuk melihat sisi mana yang mendominasi.
Gunakan baseline kecil yang bisa diulang:
Pilih target yang jelas seperti “p95 di bawah 200 ms pada 50 pengguna konkuren, error di bawah 0.5%.” Lalu ubah satu hal pada satu waktu dan uji ulang campuran permintaan yang sama.
Aktifkan slow query logging dengan ambang rendah saat pengujian pra-launch (misalnya 100–200 ms) dan catat pernyataan lengkap supaya bisa Anda salin ke klien SQL.
Tetap sementara:
Setelah menemukan pelaku terburuk, beralih ke sampling atau naikkan ambang.
Default praktis adalah beberapa kali lipat dari core CPU per instance API, sering 5–20 max open connections, dengan jumlah idle serupa, dan daur ulang koneksi setiap 30–60 menit.
Dua mode kegagalan umum:
Ingat pool bertambah seiring banyaknya instance (20 koneksi × 10 instance = 200 koneksi).
Ukur panggilan DB dalam dua bagian:
Jika sebagian besar waktu adalah pool wait, sesuaikan sizing pool, timeout, dan jumlah instance. Jika sebagian besar adalah eksekusi kueri, fokus pada EXPLAIN dan indeks.
Pastikan juga selalu menutup rows sehingga koneksi segera kembali ke pool.
Jalankan EXPLAIN (ANALYZE, BUFFERS) pada SQL yang persis dikirim API dan perhatikan:
ORDER BY)Indeks harus mencerminkan apa yang endpoint lakukan: filter + urutan.
Pendekatan default yang baik:
WHERE + ORDER BY yang sering.Gunakan partial index ketika sebagian besar trafik mengakses subset baris yang dapat diprediksi.
Contoh pola:
active = truePartial index seperti ... WHERE active = true tetap lebih kecil, lebih mungkin muat di memori, dan mengurangi overhead tulis dibanding mengindeks semuanya.
Konfirmasi dengan bahwa Postgres benar-benar menggunakannya untuk kueri bertrafik tinggi Anda.
LIMIT/OFFSET menjadi lebih lambat halaman dalam karena Postgres masih harus melewati (dan sering mengurutkan) baris yang dilewati. Halaman 500 bisa jauh lebih mahal daripada halaman 1.
Gunakan keyset (cursor) pagination:
Biasanya ya untuk endpoint daftar. Respons tercepat adalah yang tidak Anda kirimkan.
Langkah praktis:
SELECT *).include= atau fields= agar klien memilih field berat.Perbaiki red flag terbesar terlebih dulu; jangan tuning semuanya sekaligus.
Contoh: jika Anda memfilter user_id dan mengurutkan terbaru, indeks seperti (user_id, created_at DESC) sering membuat perbedaan antara p95 stabil dan lonjakan.
EXPLAINid).ORDER BY identik antar permintaan.(created_at, id) atau serupa ke dalam cursor.Ini menjaga biaya setiap halaman tetap relatif konstan seiring pertumbuhan tabel.
Anda sering mengurangi CPU Go, tekanan memori, dan tail latency hanya dengan mengecilkan payload.