Pembukaan dan Jawaban Langsung
Debugging API Routes Next.js sering kali membutuhkan bukti empiris untuk membedakan bug jaringan dari masalah resource. Kali ini, fokus kita menjawab: kenapa route upload file menyebabkan memory leak dan bagaimana memperbaikinya secara praktis? Jawabannya terletak pada stream yang tidak dibersihkan, buffer yang menumpuk, dan observabilitas yang minim. Artikel ini membahas gejala lapangan, langkah investigasi nyata, root cause, dan solusi implementatif.
Anda akan melihat bagaimana profil heap, tracing request, dan penambahan log detail memetakan situasi, serta bagaimana patch kode dengan cleanup stream dan timeout membuat API Routes stabil kembali.
1. Gejala Lapangan yang Menunjukkan Leak Memory
Tim operasi menerima alert CPU/memory tinggi pada node server saat upload file bersamaan meningkat. Gejalanya:
- Heap usage naik konsisten setiap beberapa menit sampai container restart.
- Request upload tetap terbuka lebih dari 30 detik tanpa menyelesaikan response.
- Garbage collector memakan waktu lebih lama walaupun tidak ada perubahan payload besar.
Masalah hanya muncul pada API Routes yang menerima multipart upload, sementara route lain stabil. Itu indikator kuat bahwa resource yang dikelola dalam route tidak dibebaskan dengan benar.
2. Langkah Investigasi: Profiling, Tracing, dan Log
2.1 Profiling Heap dan Stream
Jalankan profiling heap dengan node --inspect atau gunakan Heap Snapshot di Chrome DevTools. Observasi menunjukkan objek buffer dari micro-busboy (atau formidable) terus bertambah tanpa dirilis.
Saat snapshot dibuat setelah beberapa upload terputus, ada banyak ReadableStream yang masih berreferensi dari handler API, menandakan stream tidak ditutup.
2.2 Tracing Request
Tambahkan trace ID per request dan log lifecycle upload:
- Log saat
req.on('data')danreq.on('end')dijalankan. - Catat bila
file.streamterbuka tapi tidak selesai karena client memutus. - Gunakan metrics latency untuk memetakan request long-tail yang memicu leak.
Trace menunjukkan request upload yang gagal (client disconnect) tidak pernah memicu file.stream.destroy().
2.3 Observabilitas Tambahan
Tambahkan metrik berikut di route:
- Counter untuk jumlah upload sukses vs abort.
- Histogram waktu proses upload untuk mendeteksi outlier.
- Log stack trace saat handler menerima
errordari stream.
Observabilitas ini memudahkan membedakan apakah leak disebabkan request yang belum selesai atau error internal.
3. Diagnosis dan Root Cause
Root cause-nya adalah stream file tetap aktif setelah client putus sehingga node tidak melepaskan buffer dan listener.
Handler sebelumnya:
export default async function handler(req, res) {
const form = new IncomingForm();
form.parse(req, (err, fields, files) => {
if (err) {
res.status(500).json({ error: 'Upload gagal' });
return;
}
// diproses lebih lanjut
res.status(200).json({ ok: true });
});
}
Masalahnya: ketika client disconnect, callback tidak dipanggil, listener tetap terikat ke stream, dan Node tidak membebaskan memori. Tidak ada timeout atau cleanup eksplisit.
4. Solusi Teknis Terapan
4.1 Cleanup Stream dan Listener
Gunakan handler yang lebih eksplisit untuk menangani error, close, dan timeout:
import { IncomingForm } from 'formidable';
export const config = { api: { bodyParser: false } };
export default function handler(req, res) {
const form = new IncomingForm();
const timer = setTimeout(() => {
res.status(408).end('Upload timeout');
form.emit('error', new Error('timeout'));
}, 30_000);
const cleanup = () => {
clearTimeout(timer);
form.removeAllListeners();
req.socket.destroy();
};
form.once('error', (err) => {
cleanup();
res.status(500).json({ error: err.message });
});
form.once('end', () => {
cleanup();
res.status(200).json({ ok: true });
});
form.parse(req);
}
Keterangan:
- Timeout memastikan request tidak menggantung.
- cleanup() menghapus listener dan menghentikan timer setelah selesai.
- req.socket.destroy() digunakan hanya jika request tidak lagi valid, mencegah resource leak.
4.2 Cabut Referensi terhadap Objek Besar
Jika handler menyimpan referensi file di closure global, pastikan langsung dilepas setelah selesai. Contoh:
- Jangan simpan
file.bufferdi variabel module-level tanpadeletesetelah upload. - Gunakan Stream pipeline (misalnya
pipeline(form, writeStream, callback)) untuk membiarkan Node otomatis mengelola lifecycle.
4.3 Menjaga API Routes Tetap Stateless
Pastikan API Routes tidak menyimpan state upload dalam cache global. Bila perlu, gunakan store eksternal (misalnya Redis) dengan TTL pendek untuk menghindari referensi tak terpakai.
5. Observabilitas dan Pencegahan Regressi
5.1 Tip Observabilitas
- Gunakan metric ongoingUploads untuk menghitung jumlah upload aktif saat ini.
- Log trace ID untuk setiap error stream agar cepat ditelusuri.
- Gunakan distributed tracing (misalnya OpenTelemetry) untuk melihat apakah request upload menghasilkan child span yang tidak selesai.
5.2 Checklist Regresi
Setelah patch, jalankan checklist regresi berikut sebelum deploy:
- Upload file dengan koneksi stabil dan putus tiba-tiba; pastikan handler cleanup dan response timeout.
- Profil heap selama batch upload untuk memastikan tidak ada peningkatan steady-state.
- Verifikasi log mencatat cleanup timer serta error handler dijalankan saat timeout/error.
- Observasi metric ongoingUploads; seharusnya kembali ke nol setelah upload selesai.
- Uji deploy di staging dengan traffic simulasi untuk memastikan tidak ada restart karena OOM.
Penutup
Dalam kasus ini, debugging API Routes Next.js dengan memory leak saat upload file teratasi dengan observasi detail, cleanup stream eksplisit, dan monitoring tambahan. Selalu kombinasikan tracing, profiling, dan log agar root cause bisa dilihat dengan jelas. Ketika route menangani stream atau file besar, pendekatan cleanup, timeout, dan stateless design menjadi kunci untuk mencegah regresi di masa depan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!