Ketika API Function di Vercel mulai timeout saat traffic meningkat, penyebabnya tidak selalu query PostgreSQL yang lambat atau cold start. Pola yang sering ditemukan adalah koneksi PostgreSQL bocor: aplikasi membuat klien atau pool baru untuk setiap request, atau mengambil koneksi dari pool tetapi tidak mengembalikannya.

Pada arsitektur serverless, satu lonjakan traffic dapat memicu banyak instance Function secara paralel. Jika setiap instance membuka banyak koneksi database, PostgreSQL dapat mencapai kapasitas koneksinya. Request berikutnya lalu menunggu koneksi tersedia sampai akhirnya timeout. Perbaikan utamanya adalah membatasi koneksi per instance, memakai pool dengan benar, dan bila diperlukan menggunakan driver atau pooler yang dirancang untuk koneksi serverless.

Studi kasus: timeout hanya muncul saat traffic naik

Sebuah endpoint API berjalan normal pada traffic rendah. Ketika traffic naik, log mulai menunjukkan request yang berlangsung lama dan kemudian dihentikan karena melewati durasi eksekusi Function. Di sisi database, latency query tidak selalu meningkat secara proporsional, tetapi jumlah sesi koneksi aktif atau idle bertambah terus.

Gejala ini penting karena timeout Function adalah gejala akhir, bukan diagnosis. Request dapat menunggu pada beberapa tahap: inisialisasi runtime, membuka koneksi TCP/TLS, menunggu slot pool aplikasi, menunggu slot di PostgreSQL, lock transaksi, atau eksekusi query.

Indikasi pada log aplikasi dan Function

  • Durasi request meningkat terutama ketika concurrency naik, sementara endpoint yang sama cepat pada traffic rendah.
  • Log berhenti setelah pesan seperti “connecting to database” atau sebelum log setelah query pertama.
  • Error koneksi mulai muncul bersamaan, misalnya kegagalan autentikasi sementara, penolakan koneksi, atau waktu tunggu saat mendapatkan koneksi.
  • Timeout terjadi pada banyak request berbeda, bukan hanya pada satu parameter query atau satu pelanggan.
  • Setelah traffic turun, masalah bisa mereda perlahan karena koneksi yang ada akhirnya selesai atau instance lama dihentikan.

Indikasi pada metrik dan observabilitas PostgreSQL

Gunakan dashboard provider database atau observabilitas PostgreSQL untuk membandingkan tiga hal pada rentang waktu yang sama: jumlah koneksi, waktu query, dan error aplikasi. Pola kebocoran koneksi biasanya terlihat sebagai kenaikan jumlah koneksi yang mengikuti traffic, sementara penggunaan CPU atau durasi query belum tentu tinggi.

Jika Anda memiliki akses SQL yang memadai, ringkasan sesi dapat diperiksa dengan query berikut:

SELECT
  state,
  COUNT(*) AS connections
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state
ORDER BY connections DESC;

Untuk investigasi yang lebih terarah, beri nama aplikasi pada connection string, misalnya parameter application_name=vercel-api. Anda kemudian dapat memfilter pg_stat_activity berdasarkan nama tersebut. Hindari menyalin nilai rahasia connection string ke log.

Jumlah koneksi maksimum tidak memiliki angka universal. Batas bergantung pada konfigurasi PostgreSQL, tier provider, koneksi yang dicadangkan untuk proses internal, aplikasi lain, dan pooler yang digunakan. Fokuslah pada tren koneksi serta kapasitas yang dilaporkan oleh provider Anda.

Membedakan cold start dan kehabisan koneksi PostgreSQL

Cold start terjadi ketika instance Function baru perlu dimuat dan menginisialisasi kode sebelum menangani request. Dampaknya sering terlihat sebagai latensi tambahan pada request pertama suatu instance. Cold start dapat muncul setelah periode tidak aktif atau ketika platform perlu menambah instance.

Kehabisan koneksi PostgreSQL berbeda: masalahnya cenderung memburuk saat banyak request berjalan bersamaan. Request pada instance yang sudah hangat pun dapat ikut lambat karena tetap harus menunggu koneksi database.

KarakteristikCold startTekanan atau kebocoran koneksi
Pemicu utamaInstance baru diinisialisasiConcurrency dan jumlah instance meningkat
Pola latensiSering pada request awal instanceMeningkat pada banyak request secara bersamaan
Metrik databaseJumlah koneksi tidak harus naik tajamKoneksi aktif/idle meningkat atau mendekati kapasitas
Lokasi waktu tungguBootstrap runtime, import, inisialisasiMendapatkan koneksi, handshake, pool, atau database

Keduanya dapat terjadi bersamaan. Cold start bahkan dapat memperburuk tekanan koneksi karena setiap instance baru menjalankan kode inisialisasi database. Karena itu, ukur durasi koneksi dan query secara terpisah, bukan hanya durasi request total.

Root cause: pola koneksi yang keliru di Function

Pola berikut membuat satu klien PostgreSQL baru pada setiap request dan tidak menjamin koneksi ditutup jika query gagal. Pada traffic rendah, masalah mungkin tidak langsung terlihat. Pada concurrency tinggi, sesi yang tertinggal dapat menumpuk.

import { Client } from "pg";

export default async function handler(req: Request): Promise<Response> {
  const client = new Client({
    connectionString: process.env.DATABASE_URL
  });

  await client.connect();
  const result = await client.query(
    "SELECT id, email FROM users WHERE id = $1",
    ["user_123"]
  );

  // Jika query di atas melempar error, client.end() tidak pernah dipanggil.
  await client.end();

  return Response.json(result.rows[0]);
}

Menambahkan client.end() di blok finally memang mencegah kebocoran pada contoh tersebut, tetapi membuat koneksi baru per request tetap mahal. Handshake, autentikasi, dan penggunaan slot koneksi akan berulang untuk setiap request.

Kesalahan lain yang umum adalah membuat new Pool() di dalam handler. Pool tersebut hanya hidup untuk request itu, sehingga bukan benar-benar berbagi koneksi antar-request pada instance yang sama. Lebih buruk lagi, setiap instance serverless mempunyai pool sendiri.

Perbaikan: pool per instance dan pelepasan client yang konsisten

Untuk runtime Node.js yang mendukung koneksi PostgreSQL langsung, buat satu Pool pada scope modul. Scope ini dapat digunakan ulang oleh request berikutnya pada instance Function yang sama. Ini bukan singleton global untuk seluruh deployment Vercel; platform dapat menjalankan banyak instance, sehingga ukuran pool per instance harus tetap kecil dan disengaja.

import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: Number(process.env.PG_POOL_MAX ?? "2"),
  connectionTimeoutMillis: 3_000,
  idleTimeoutMillis: 10_000
});

export default async function handler(req: Request): Promise<Response> {
  const result = await pool.query(
    "SELECT id, email FROM users WHERE id = $1",
    ["user_123"]
  );

  return Response.json(result.rows[0]);
}

pool.query() cocok untuk satu query tanpa transaksi karena driver meminjam dan mengembalikan koneksi secara internal. Untuk transaksi atau beberapa query yang harus memakai sesi yang sama, ambil client dari pool dan selalu lepaskan dalam finally.

import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: Number(process.env.PG_POOL_MAX ?? "2")
});

export async function createOrder(userId: string, total: number) {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    const order = await client.query(
      "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
      [userId, total]
    );

    await client.query("COMMIT");
    return order.rows[0];
  } catch (error) {
    await client.query("ROLLBACK").catch(() => undefined);
    throw error;
  } finally {
    client.release();
  }
}

Jangan memanggil pool.end() pada akhir setiap request. Itu menutup pool yang seharusnya dapat dipakai ulang oleh instance hangat dan dapat menambah churn koneksi. Penutupan pool lebih relevan untuk proses long-running yang memiliki siklus shutdown yang jelas, bukan lifecycle request serverless.

Memilih pool langsung atau driver serverless-compatible

Pool langsung dengan driver PostgreSQL Node.js dapat sesuai bila runtime dan provider database mendukung koneksi TCP yang stabil dari Function. Namun, model serverless tetap memiliki risiko ledakan koneksi karena banyak instance dapat aktif serentak.

Alternatifnya adalah memakai layanan pooler eksternal atau driver serverless-compatible yang direkomendasikan provider database. Beberapa layanan menyediakan koneksi berbasis HTTP atau WebSocket agar Function tidak perlu mempertahankan banyak koneksi TCP langsung. Gunakan opsi ini ketika workload sangat bursty, batas koneksi ketat, atau provider secara eksplisit merekomendasikannya untuk Vercel.

Trade-off pooler perlu diperiksa sebelum migrasi. Mode pooling tertentu tidak mempertahankan state sesi antar-query atau antar-transaksi. Fitur seperti temporary table, beberapa perintah SET, prepared statement yang bergantung pada sesi, atau transaksi panjang dapat membutuhkan penyesuaian. Ikuti dokumentasi provider dan uji alur transaksi aplikasi Anda.

Langkah investigasi yang dapat diulang

  1. Tentukan titik waktu insiden. Cocokkan timeout Vercel, lonjakan request, error aplikasi, dan metrik PostgreSQL dalam zona waktu yang sama.
  2. Tambahkan pengukuran per tahap. Catat durasi mendapatkan koneksi, durasi query, dan durasi request total. Jangan log SQL mentah atau parameter sensitif.
  3. Audit pembuatan klien. Cari new Client, new Pool, ORM client baru, atau fungsi koneksi yang dipanggil dari handler.
  4. Audit jalur error. Pastikan semua client hasil pool.connect() memiliki release() dalam finally, termasuk jalur rollback transaksi.
  5. Periksa sesi database. Lihat state koneksi, aplikasi asal koneksi, serta query atau transaksi yang berjalan lama.
  6. Uji dengan concurrency terkontrol. Jalankan load test di lingkungan aman. Amati apakah jumlah koneksi bertambah tanpa kembali stabil setelah request selesai.
  7. Periksa query dan lock. Jika koneksi tidak tinggi tetapi request tetap menunggu, telusuri query lambat, indeks yang hilang, dan lock transaksi sebelum menyimpulkan ada kebocoran.

Verifikasi setelah rilis perbaikan

Rilis perbaikan sebaiknya diverifikasi dengan metrik, bukan hanya memastikan endpoint kembali merespons. Bandingkan sebelum dan sesudah pada tingkat traffic yang setara.

  • Jumlah koneksi PostgreSQL tidak lagi terus naik mengikuti setiap request dan kembali ke kondisi stabil setelah lonjakan berakhir.
  • Error saat koneksi serta timeout Function turun atau hilang pada skenario load test yang sama.
  • Durasi mendapatkan koneksi tidak mendominasi durasi request.
  • Latency query tetap dipantau; perbaikan koneksi tidak akan menyelesaikan query yang memang lambat.
  • Endpoint transaksi tetap benar secara fungsional, terutama setelah menambahkan rollback dan finally.

Jika memungkinkan, gunakan deployment bertahap dan observasi metrik database segera setelah traffic nyata masuk. Rollback cepat lebih aman daripada membiarkan koneksi terus menumpuk.

Batasan solusi dan checklist pencegahan

Pool per instance mengurangi churn koneksi, tetapi tidak menghapus efek concurrency serverless. Misalnya, pool kecil yang aman untuk satu instance dapat tetap menjadi besar secara total ketika banyak instance diskalakan bersamaan. Karena itu, ukuran pool harus dihitung bersama kapasitas database, jumlah layanan lain, dan pola scale-out aplikasi.

  • Buat pool atau klien reusable di scope modul, bukan di dalam handler request.
  • Gunakan pool.query() untuk query tunggal tanpa transaksi.
  • Gunakan try/catch/finally dan client.release() untuk transaksi.
  • Batasi ukuran pool per instance secara konservatif dan konfigurasikan melalui environment variable.
  • Gunakan timeout koneksi yang eksplisit agar request tidak menunggu terlalu lama tanpa diagnosis.
  • Hindari transaksi panjang, query yang menahan lock, dan operasi eksternal di dalam transaksi.
  • Tambahkan identitas aplikasi pada koneksi agar sesi mudah ditelusuri di database.
  • Pantau koneksi, error koneksi, latency query, dan timeout Function sebagai satu rangkaian sinyal.
  • Pertimbangkan pooler atau driver serverless-compatible untuk workload bursty dan batas koneksi ketat.

Dengan pendekatan ini, timeout tidak diperlakukan sebagai masalah Vercel semata. Investigasi menghubungkan lifecycle Function, concurrency, pool aplikasi, dan kapasitas PostgreSQL sehingga perbaikan yang dipilih benar-benar menutup sumber kebocoran.