Mengoptimasi performa sampai produksi: profiling untuk menemukan bottleneck nyata, connection pooling untuk database, isolasi level database, dan proses tuning yang berbasis pengukuran bukan tebakan

Di episode 9-21 kalian membangun cache, queue, dan scaling. Episode 25 membawa performa ke tingkat lanjutan: saat cache sudah dipasang dan instance sudah diskala, masih ada lapisan optimasi yang lebih dalam — profiling, connection pooling, dan analisis bottleneck. Inilah pekerjaan sehari-hari senior backend engineer.
Mengapa penting? Karena di produksi, performa adalah uang dan pengalaman: API yang lambat kehilangan user, query yang boros menaikkan biaya database, dan connection pool yang salah membuat database kehabisan koneksi justru saat traffic naik. Episode ini mengajarkan cara menemukan bottleneck dengan data — bukan tebakan.
Aturan emas tuning: semua keputusan berbasis pengukuran. Identifikasi bottleneck dengan cara yang sama seperti diagnosa medis — ukur dulu, baru tindak.
curl -s -o /dev/null -w "total: %{time_total}s\n" http://localhost:3000/v1/products
curl -s -o /dev/null -w "connect: %{time_connect}s\n" http://localhost:3000/v1/productsPerbedaan time_connect (jaringan) vs time_total (server + jaringan) memberi petunjuk pertama: lambat di jaringan atau di aplikasi?
Jika time_total jauh lebih besar dari time_connect, bottleneck ada di aplikasi/database — bukan jaringan.
Profiling menemukan di mana CPU dan waktu dihabiskan. Tiga level:
Node.js punya profiler bawaan:
node --cpu-prof --cpu-prof-dir=/tmp/prof dist/server.js
# hasil .cpuprofile dibuka di Chrome DevTools / speedscopeDatabase sering jadi bottleneck terbesar. PostgreSQL menyediakan cara melihat query paling lambat dan paling sering:
SELECT
calls,
round(total_exec_time::numeric, 2) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;Aktifkan pg_stat_statements di postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'Alat seperti async-profiler (Java), pprof (Go), dan perf menghasilkan flamegraph — peta visual di mana waktu menumpuk. Flamegraph dibaca: lebar = waktu; fungsi paling lebar adalah target optimasi.
Note
Jangan optimize berdasarkan perasaan — optimize berdasarkan profil. Lebih dari setengah "optimasi" tanpa pengukuran sia-sia atau bahkan merugikan. Workflow yang benar: ukur → temukan hotspot → perbaiki → ukur lagi. Simpan baseline sebelum perubahan, bandingkan setelahnya.
Setiap query butuh koneksi ke database — dan membuat koneksi baru sangat mahal (handshake TCP + auth + proses). Connection pool menjaga sekumpulan koneksi siap pakai.
import { Pool } from "pg"
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // koneksi maksimal
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
})
router.get("/v1/products", async (_req, res) => {
const { rows } = await pool.query(
"SELECT * FROM products WHERE stock > 0 LIMIT 50",
)
res.json({ data: rows })
})Kesalahpahaman umum: "semakin banyak koneksi semakin cepat". Salah — koneksi melampaui batas justru memperlambat: tiap koneksi memakan memori, dan database menyentuh disk dalam satu waktu (episode 5). Aturan praktis yang terkenal:
ukuran pool ≈ (jumlah core CPU) × 2 + (spare untuk I/O disk)Contoh: database 8 core → pool sekitar 16-24. Tambahkan pemantauan: jika idle in transaction dan waiting naik, pool terlalu kecil; jika CPU database tidak terpakai dan koneksi menumpuk, pool terlalu besar.
Kesalahan paling mahal: koneksi diambil tapi tidak pernah dikembalikan → pool habis → aplikasi timeout semua request. Selalu try/finally:
import { pool } from "./db.js"
async function getProduct(id: number) {
const client = await pool.connect()
try {
return await client.query(
"SELECT * FROM products WHERE id = $1", [id])
} finally {
client.release()
}
}Saat API lambat, periksa berurutan:
CDN belum dipakai? Konten statis lambat? Episode 9.
Profiling kode: ada loop O(n²), serialisasi besar, atau blocking sync? Episode 25 bagian profiling.
Query lambat di pg_stat_statements. Optimasi: index (episode 5), pagination (episode 4), cache (episode 9).
CPU tinggi? Scaling (episode 21). Koneksi habis? Pool. Latency jaringan? Region/CDN (episode 16).
Contoh alur diagnosa nyata:
Gejala: GET /v1/orders lambat 2 detik, CPU app normal.
1. curl -w → time_total 2s, time_connect 20ms → bottleneck di app/db
2. pg_stat_statements → query orders tanpa index, 1.8s eksekusi
3. EXPLAIN ANALYZE → sequential scan (episode 5)
4. CREATE INDEX idx_orders_user → latency turun ke 80msSatu transaksi panjang bisa memblokir yang lain. Pahami isolation level (episode 5) dan hindari transaksi yang membungkus I/O lambat:
PENYEBAB LAMBAT:
BEGIN;
SELECT ...; -- baca cepat
await callExternalApi(); -- 2 detik menahan transaksi!
UPDATE ...;
COMMIT;Transaksi menahan lock dan memblokir baris lain (episode 5). Aturan: transaksi sependek mungkin — I/O eksternal di luar transaksi; lock dipecah; isolasi disesuaikan kebutuhan (biasanya READ COMMITTED sudah cukup).
Warning
Transaksi yang membungkus panggilan eksternal (HTTP/queue) adalah salah satu penyebab "database menggantung" paling umum: aplikasi menahan transaksi sambil menunggu layanan lain, dan layanan lain menunggu database. Pecah transaksi: tulis dulu, proses eksternal setelah commit, lalu update status.
Tuning tanpa load test tidak terverifikasi. k6 adalah alat load testing modern:
import http from "k6/http"
import { check, sleep } from "k6"
export const options = {
stages: [
{ duration: "1m", target: 50 }, // naik ke 50 user
{ duration: "2m", target: 50 }, // tahan
{ duration: "1m", target: 0 }, // turun
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95% request < 500ms
},
}
export default function () {
const res = http.get("http://localhost:3000/v1/products")
check(res, { "status 200": (r) => r.status === 200 })
sleep(0.1)
}bunx k6 run load-test.jsHasilnya memberi jawaban objektif: berapa kapasitas sebenarnya, dan di mana titik patah — sebelum event besar (episode 21), bukan setelahnya.
Tip
Load test yang baik mengukur response time percentiles (p50, p95, p99), bukan rata-rata — rata-rata menyembunyikan lonjakan. p95 di bawah ambang berarti mayoritas user nyaman. Simpan hasil setiap load test sebagai baseline: begitulah kalian tahu apakah perubahan benar-benar memperbaiki atau merusak.
Menebak bottleneck = buang waktu. Profil dulu.
Koneksi berlebih memperlambat database. Ikuti aturan core × 2 + spare.
Menahan lock sambil menunggu HTTP = menggantung. Pecah transaksi.
p50 yang baik bisa menyembunyikan p99 yang buruk. Ukur percentile.
Index yang salah tidak dipakai. EXPLAIN ANALYZE selalu.
Episode 25 menuntaskan performa: prinsip ukur-jangan-menebak, profiling kode dan database, connection pooling yang tepat, analisis bottleneck berlapis, isolasi level, dan load testing berbasis percentile.
Inti yang harus dibawa pulang:
pg_stat_statements untuk query, CPU profiler untuk kode.Di episode 26 selanjutnya kita akan melihat gambaran besar industri: ekosistem & tren modern 2026 — stack yang dominan dan arah backend ke depan. Sampai jumpa di episode 26!