
Health Check dan Graceful Shutdown di Node.js: Dua Hal Kecil yang Menyelamatkan Deploy Malam Saya
Cara implementasi health check endpoint dan graceful shutdown di aplikasi Node.js produksi, lengkap dengan kode yang benar-benar dipakai di sistem multi-tenant saya.
Proyek SaaS multi-tenant yang saya kelola dulu punya gejala aneh setiap kali deploy: request gagal secara acak selama 30-60 detik, tapi tidak ada error yang jelas di log. Baru setelah dua kali kena komplain tenant karena webhook mereka drop, saya sadar masalahnya bukan di kode bisnis — aplikasi kami mati mendadak saat pod di-replace, dan Kubernetes menganggap pod baru "sehat" padahal database belum siap. Health check dan graceful shutdown adalah dua hal yang kelihatan sepele di tutorial, tapi di produksi inilah bedanya deploy yang mulus dan deploy yang bikin saya bangun jam 2 pagi.
Artikel ini saya tulis dari pengalaman memperbaiki persis masalah itu — bukan teori. Saya pakai Node.js dengan Express dan PostgreSQL (via Prisma), tapi polanya sama untuk Fastify atau NestJS.
Kenapa SIGTERM yang Didiamkan Itu Berbahaya
Secara default, saat Kubernetes atau Docker menghentikan container, mereka mengirim sinyal SIGTERM dan memberi waktu tunggu (default 30 detik di Kubernetes) sebelum SIGKILL. Kalau aplikasi Node.js Anda tidak menangani SIGTERM, yang terjadi adalah proses mati seketika — di tengah request yang sedang diproses, di tengah query yang setengah jalan, di tengah transaksi database.
Yang paling sering saya lihat di codebase orang: aplikasi Express yang hanya app.listen(3000) dan selesai. Tidak ada penanganan sinyal sama sekali. Di development ini tidak terasa, karena kita selalu Ctrl+C dan tidak ada traffic. Di produksi dengan rolling update, setiap deploy berarti beberapa request mati diam-diam.

Solusinya ada dua langkah: berhenti menerima request baru, lalu tunggu request yang sedang berjalan selesai. Detailnya di bawah.
Health Check yang Jujur, Bukan yang Cantik
Kubernetes punya tiga jenis probe: liveness, readiness, dan startup. Kesalahan yang saya lakukan dulu: satu endpoint /health dipakai untuk semuanya. Akibatnya, saat database sempat lag 5 detik, pod dianggap tidak hidup dan di-restart — padahal yang seharusnya terjadi cuma pod dicabut sementara dari load balancer.
Pemisahan yang saya pakai sekarang:
GET /healthz— liveness. Cuma cek proses masih hidup dan event loop tidak macet. Tidak cek database.GET /readyz— readiness. Cek koneksi database (query ringan sepertiSELECT 1), dan koneksi ke dependency eksternal yang kritis.
Kenapa liveness tidak boleh cek database? Karena kalau database down 30 detik, semua pod akan restart bersamaan, dan saat database pulih Anda mendapat thundering herd dari ratusan pod yang reconnect serempak. Saya pernah mengalami ini — outage database 1 menit berubah menjadi chaos 10 menit gara-gara liveness probe yang terlalu ambisius.
Contoh implementasinya di Express:
// liveness: cepat, tidak menyentuh dependency
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// readiness: jujur soal dependency
app.get('/readyz', async (req, res) => {
try {
await prisma.$queryRaw`SELECT 1`;
res.status(200).json({ status: 'ready' });
} catch (err) {
// pod tetap hidup, tapi jangan kirim traffic ke sini
res.status(503).json({ status: 'not-ready' });
}
});
Satu trik tambahan yang berguna: setelah menerima SIGTERM, langsung balikkan /readyz ke 503. Ini memberi tahu Kubernetes dan load balancer untuk berhenti mengirim request baru jauh sebelum proses benar-benar mati.

Implementasi Graceful Shutdown yang Benar
Urutan penanganan SIGTERM yang saya pakai, sudah teruji di production hampir dua tahun:
- Balikkan
/readyzjadi 503 supaya tidak ada request baru. - Tutup HTTP server dengan
server.close()— ini menunggu semua koneksi idle, tapi request yang sedang berjalan tetap diberi waktu. - Tutup koneksi database (Prisma pool).
- Exit dengan kode yang benar.
Kode lengkapnya:
const server = app.listen(3000);
let isShuttingDown = false;
// dipakai oleh handler /readyz
app.get('/readyz', (req, res) => {
if (isShuttingDown) return res.status(503).json({ status: 'shutting-down' });
// ... cek database seperti di atas
});
async function shutdown(signal) {
console.log(`Menerima ${signal}, mulai graceful shutdown`);
isShuttingDown = true;
const forceExit = setTimeout(() => {
console.error('Timeout shutdown, force exit');
process.exit(1);
}, 20000); // harus < terminationGracePeriodSeconds (30s default)
try {
await new Promise((resolve) => server.close(resolve));
await prisma.$disconnect();
clearTimeout(forceExit);
console.log('Shutdown bersih, exit');
process.exit(0);
} catch (err) {
console.error('Gagal shutdown bersih', err);
process.exit(1);
}
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Tiga detail yang sering dilewatkan:
- Timeout 20 detik, bukan 30. Angka ini harus selalu lebih kecil dari
terminationGracePeriodSecondsdi Kubernetes. Kalau sama atau lebih besar, Kubernetes akanSIGKILLduluan sebelum cleanup Anda selesai — graceful shutdown jadi sia-sia. server.close()tidak memutus koneksi keep-alive. Kalau ada load balancer yang memegang koneksi keep-alive,server.close()bisa menggantung selamanya. Solusinya: lacak socket aktif dandestroy()mereka setelah grace period pendek (misal 5 detik).- Jangan lupa job background. Kalau Anda punya worker queue atau cron internal, daftarkan juga di proses shutdown — berhenti ambil job baru, tunggu job yang sedang diproses.
Bagaimana Ini Berhubungan dengan Strategi Deploy
Graceful shutdown hanya berfungsi kalau strategi deploy Anda benar-benar memberi kesempatan pod lama untuk drain. Ini berkaitan erat dengan pilihan antara rolling update dan blue-green — saya sudah tuliskan perbandingan keduanya di artikel tentang zero-downtime deployment, termasuk kenapa grace period dan probe tuning jauh lebih kritis di rolling update.
Satu hal yang layak disebut lagi di sini: di rolling update, pastikan maxUnavailable: 0 di Deployment Anda. Tanpa itu, Kubernetes boleh membunuh pod lama sebelum pod baru siap — graceful shutdown Anda pun tidak akan menyelamatkan request yang sudah tidak punya pod tujuan.
Kesalahan-Kesalahan yang Masih Saya Temui di Code Review
Beberapa pola berulang yang hampir tiap minggu saya lihat:
- Endpoint
/healthyang mengembalikan200hard-coded, lalu dipakai sebagai readiness probe. Pod dianggap siap sebelumSELECT 1pertama berhasil, dan request pertama setelah deploy gagal denganPrismaClientInitializationError. - Penanganan
SIGTERMyang hanyaprocess.exit(0)— itu namanya shutdown cepat, bukan graceful. - Shutdown handler yang menunggu database tanpa timeout. Saya pernah lihat pod menggantung sampai di-
SIGKILLkarena sebuah query kartesian lambat (kebiasaan baca EXPLAIN ANALYZE membantu menghindari query seperti ini sejak awal). - Liveness probe dengan
initialDelaySecondsterlalu kecil, sehingga aplikasi yang start lambat langsung di-restart berulang sebelum sempat jalan.
Takeaway yang Bisa Langsung Anda Pakai
Kalau cuma ada waktu 30 menit minggu ini untuk memperbaiki satu hal di service Anda, ini urutan yang saya sarankan:
- Pisahkan
/healthz(liveness, tanpa cek dependency) dan/readyz(readiness, cek database). - Tambahkan flag
isShuttingDownyang membalikkan/readyzke 503 saatSIGTERMditerima. - Implementasikan
shutdown()denganserver.close()+prisma.$disconnect()+ timeout yang lebih kecil dari grace period cluster Anda. - Uji secara lokal:
kubectl port-forwardke pod, jalankankubectl delete pod, lalu tembak request denganheyatauabsambil memperhatikan apakah ada yang gagal.
Titik nomor 4 itu yang paling sering dilewati. Graceful shutdown yang tidak pernah diuji bukan graceful shutdown — itu harapan. Uji sebelum produksi yang mengujikan untuk Anda.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.


