Kebanyakan tutorial OTP di Node.js berhenti di “email berhasil terkirim”, padahal di production, masalah baru justru mulai dari sana. OTP masuk spam, terlambat sampai, atau endpoint-nya bisa dibrute force. Artikel ini menjelaskan ketiga masalah tersebut dan cara mengatasinya secara teknis.
Anda sudah bisa kirim email OTP dari aplikasi Node.js. Nodemailer dikonfigurasi, Gmail SMTP terhubung, OTP muncul di inbox saat testing lokal. Semua berjalan lancar.
Sampai masuk production.
Di production, tiga masalah berbeda muncul bergantian. OTP masuk folder spam user. OTP terlambat sampai sampai user sudah keburu klik “kirim ulang”. Dan kalau ada yang iseng, endpoint kirim OTP bisa dipanggil ribuan kali tanpa hambatan.
Ketiganya bukan bug di kode Anda. Semuanya adalah keputusan arsitektur yang tidak kelihatan masalah saat development, tapi rapuh di production.
Daftar Isi
Kenapa OTP Email dari Node.js Masuk Spam?
Masalahnya bukan di Nodemailer. Nodemailer hanya mengantarkan email dari aplikasi ke SMTP server. Apakah email itu berakhir di inbox atau spam, itu keputusan filter di sisi penerima, yang mengevaluasi tiga hal secara terpisah: reputasi IP server pengirim, autentikasi domain lewat SPF dan DKIM, dan konten email itu sendiri.
Konfigurasi Nodemailer yang benar hanya menyelesaikan satu bagian kecil dari persamaan ini.
Masalah paling umum: IP reputasi yang buruk.
Kalau Anda pakai shared SMTP, berarti satu IP dipakai bersama banyak pengguna lain dari provider yang sama. Kalau satu pengguna lain di IP tersebut pernah kirim spam massal, reputasi IP itu sudah rusak untuk semua orang, termasuk Anda. Email OTP Anda dievaluasi pakai reputasi yang bukan milik Anda.
Ini kelemahan struktural dari shared SMTP yang tidak bisa diatasi hanya dengan konfigurasi yang lebih rapi.
Masalah kedua: SPF atau DKIM belum dikonfigurasi.
SPF adalah DNS record yang mendaftarkan IP mana saja yang boleh kirim email atas nama domain Anda. DKIM menambahkan tanda tangan digital yang membuktikan email tidak diubah dalam perjalanan dari server pengirim ke server penerima.
Tanpa keduanya, filter spam tidak punya alasan untuk mempercayai email dari domain Anda. Untuk email OTP yang kritikal, tidak punya DKIM adalah risiko yang tidak perlu ditanggung.
Cara verifikasi paling cepat: kirim email ke alamat test di mail-tester.com. Skor di bawah 8 dari 10 berarti ada yang perlu diperbaiki, dan hasilnya sudah menunjukkan dengan tepat di bagian mana.
Gmail SMTP Bukan Pilihan yang Aman untuk OTP di Production
Menggunakan Gmail SMTP karena sudah familiar adalah keputusan yang masuk akal saat development. Di production, ini masalah yang perlu diselesaikan sebelum launch.
Gmail free SMTP punya limit 100 email per hari via SMTP. Bukan 500, bukan unlimited: 100. Untuk aplikasi dengan ratusan registrasi atau transaksi per hari, limit ini bisa terlampaui sebelum jam makan siang.
Selain soal limit, Google mendeteksi pola koneksi yang menyerupai account hijacking. Server production yang berjalan di cloud, terkoneksi dari IP yang tidak pernah dipakai sebelumnya dengan akun Gmail tersebut, bisa langsung diblokir tanpa peringatan. Email tidak dikirim, tidak ada error yang informatif di log, dan user tidak menerima OTP-nya.
IP Gmail juga adalah shared IP yang reputasinya tidak bisa Anda kontrol.
Untuk production OTP, gunakan dedicated SMTP provider. KIRIM.EMAIL Dev menyediakan dedicated IP dengan server yang berlokasi di Indonesia. Artinya, latency ke user Indonesia lebih rendah dibanding provider yang servernya di Singapore atau US, dan reputasi IP tidak tercampur dengan pengguna lain. Kalau Anda butuh opsi internasional, Postmark dan Amazon SES juga solid, dengan free tier masing-masing 100 email per hari dan 62.000 email per bulan di tahun pertama.
Cara Setup Nodemailer dengan DKIM agar OTP Tidak Masuk Spam
Nodemailer mendukung DKIM signing langsung di level transporter, tanpa middleware tambahan.
const nodemailer = require('nodemailer');
const fs = require('fs');
const transporter = nodemailer.createTransport({
host: 'smtp.kirim.email', // ganti sesuai provider Anda
port: 465,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
dkim: {
domainName: 'domainanda.com',
keySelector: 'ke1',
privateKey: fs.readFileSync('./dkim-private.pem', 'utf8'),
},
});
Private key DKIM didapat dari SMTP provider Anda saat setup domain. Setelah key ini dipasang di DNS dan konfigurasi Nodemailer diupdate seperti di atas, setiap email yang keluar dari aplikasi sudah ter-sign secara otomatis.
Buat transporter ini sekali saat aplikasi start, bukan di dalam fungsi yang dipanggil setiap kirim email. Ini penting untuk performa yang akan dibahas di bagian berikutnya.
OTP Delay: Kenapa Terlambat dan Cara Mengatasinya
OTP yang terlambat sampai biasanya disebabkan oleh satu hal: aplikasi membuka koneksi SMTP baru setiap kali mengirim email.
Membuka koneksi SMTP baru berarti TLS handshake baru dari awal. Proses ini memakan waktu antara 100 sampai 500 milidetik per koneksi, tergantung jarak server dan kondisi jaringan. Untuk satu email tidak terasa. Tapi saat ada puluhan user yang request OTP hampir bersamaan, koneksi baru yang dibuka satu per satu ini menciptakan antrian yang terasa seperti “delay jaringan”.
Padahal bukan jaringannya yang lambat. Itu overhead dari koneksi yang tidak dikelola dengan benar.
Solusinya adalah connection pooling: Nodemailer mempertahankan beberapa koneksi SMTP yang tetap terbuka dan siap dipakai ulang.
const transporter = nodemailer.createTransport({
host: 'smtp.kirim.email',
port: 465,
secure: true,
pool: true, // aktifkan connection pooling
maxConnections: 5, // maksimal 5 koneksi bersamaan
maxMessages: 100, // tiap koneksi mengirim maks 100 email sebelum reconnect
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
Dengan pooling aktif, Nodemailer mempertahankan sampai 5 koneksi terbuka ke SMTP server. OTP yang masuk ke antrian tidak perlu menunggu TLS handshake baru: langsung dikirim lewat koneksi yang sudah siap.
Satu aturan yang wajib diikuti: buat transporter sekali, reuse di seluruh aplikasi. Jangan buat nodemailer.createTransport() di dalam fungsi sendOtp(), karena setiap kali fungsi itu dipanggil, pool baru dibuat dan pool lama dibuang. Efeknya sama saja seperti tidak pakai pooling sama sekali.
Cara yang benar: export satu instance transporter dari modul terpisah, lalu import di mana pun dibutuhkan.
// mailer.js
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 465,
secure: true,
pool: true,
maxConnections: 5,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
module.exports = transporter;
// otp.service.js
const transporter = require('./mailer');
async function sendOtp(email, otp) {
await transporter.sendMail({
from: '"Nama Aplikasi" <[email protected]>',
to: email,
subject: 'Kode OTP Anda',
text: `Kode OTP Anda: ${otp}. Berlaku selama 60 detik.`,
html: `<p>Kode OTP Anda: <strong>${otp}</strong></p><p>Berlaku selama 60 detik.</p>`,
});
}
Jangan Lupa Rate Limiting: OTP Tanpa Proteksi Adalah Celah Keamanan
OTP 6 digit numerik punya 1.000.000 kombinasi. Tanpa rate limiting, endpoint kirim dan verifikasi OTP bisa dipanggil terus-menerus. Secara teori, kode yang benar bisa ditemukan lewat brute force sebelum OTP expire, terutama kalau window expiry-nya cukup panjang.
Rate limiting untuk OTP perlu dipasang di dua layer.
Layer pertama: batasi request kirim OTP per IP.
const rateLimit = require('express-rate-limit');
const otpSendLimiter = rateLimit({
windowMs: 60 * 1000, // window 1 menit
max: 3, // maks 3 request per IP per menit
message: { error: 'Terlalu banyak permintaan OTP. Tunggu sebentar.' },
});
app.post('/auth/send-otp', otpSendLimiter, sendOtpHandler);
Layer kedua: batasi percobaan verifikasi OTP.
const otpVerifyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // window 15 menit
max: 5, // maks 5 percobaan verifikasi
message: { error: 'Terlalu banyak percobaan. OTP ini tidak valid lagi.' },
});
app.post('/auth/verify-otp', otpVerifyLimiter, verifyOtpHandler);
Layer pertama mencegah spam ke endpoint kirim OTP. Layer kedua mencegah brute force pada kode yang sudah dikirim. Keduanya perlu dipasang bersamaan karena menyerang dua titik yang berbeda.
Berapa Lama OTP Seharusnya Berlaku?
Jawabannya bergantung pada konteks transaksi, bukan satu angka global yang berlaku untuk semua kasus.
Untuk autentikasi dasar seperti login atau konfirmasi email, 60 sampai 120 detik adalah rentang yang wajar. Cukup lama untuk user membuka email, cukup singkat untuk meminimalkan risiko replay attack.
Untuk aksi yang lebih sensitif seperti reset password, perubahan nomor telepon, atau konfirmasi pembayaran, 30 sampai 60 detik lebih tepat. Semakin besar dampak dari aksi tersebut kalau disalahgunakan, semakin pendek window expiry yang masuk akal.
Cantumkan waktu berlaku secara eksplisit di dalam email OTP-nya. “Kode ini berlaku selama 60 detik” mengurangi friction user dan mengurangi permintaan kirim ulang yang tidak perlu.
Dan satu hal yang sering terlewat: invalidasi OTP segera setelah berhasil dipakai. Kalau kode sudah diverifikasi, hapus atau tandai sebagai used di database. OTP yang masih valid setelah dipakai adalah celah yang tidak perlu ada.
Ringkasan: Checklist Sebelum Deploy OTP ke Production
Sebelum OTP system Anda masuk production, pastikan hal-hal ini sudah beres:
- Pakai dedicated SMTP provider, bukan Gmail SMTP
- SPF dan DKIM sudah dikonfigurasi di DNS domain Anda
- Verifikasi skor dengan mail-tester.com, target minimal 9/10
- Transporter Nodemailer dibuat sekali dengan pooling aktif
- Rate limiting dipasang di endpoint kirim dan verifikasi OTP
- Expiry time OTP sesuai sensitivitas transaksi (60-120 detik untuk dasar, 30-60 detik untuk sensitif)
- OTP diinvalidasi segera setelah berhasil diverifikasi
FAQ
Kenapa OTP email dari Node.js masuk folder spam padahal kode sudah benar?
Masalahnya bukan di kode. Filter spam mengevaluasi reputasi IP server SMTP yang dipakai dan autentikasi domain lewat SPF dan DKIM. Kalau Anda pakai Gmail SMTP atau shared SMTP dengan reputasi buruk, email bisa masuk spam terlepas dari kode yang sudah benar. Solusinya: pindah ke dedicated SMTP dan setup SPF/DKIM.
Apakah aman menggunakan Gmail SMTP untuk kirim OTP di aplikasi production?
Tidak disarankan untuk production. Gmail free SMTP punya limit 100 email per hari via SMTP, dan koneksi bisa diblokir tanpa peringatan kalau server terkoneksi dari IP yang tidak familiar dengan akun Gmail tersebut. Untuk production, gunakan dedicated SMTP provider yang memberikan dedicated IP dan limit yang sesuai volume aplikasi.
Berapa lama OTP email seharusnya berlaku?
60 sampai 120 detik untuk autentikasi dasar seperti login. 30 sampai 60 detik untuk aksi sensitif seperti reset password atau konfirmasi pembayaran. Setelah OTP berhasil diverifikasi, invalidasi segera di database agar tidak bisa dipakai ulang.
Bagaimana cara mencegah brute force pada endpoint OTP di Node.js?
Pasang dua layer rate limiting menggunakan express-rate-limit: satu untuk endpoint kirim OTP (maksimal 3 request per menit per IP) dan satu untuk endpoint verifikasi (maksimal 5 percobaan per 15 menit). OTP 6 digit punya 1.000.000 kombinasi, dan tanpa rate limiting, brute force sebelum expiry secara teknis memungkinkan.
Apa penyebab OTP delay dan bagaimana cara mengatasinya?
Penyebab paling umum adalah aplikasi membuka koneksi SMTP baru setiap kali mengirim email. Setiap koneksi baru butuh TLS handshake yang memakan 100 sampai 500 milidetik. Saat banyak user request OTP bersamaan, overhead ini menumpuk jadi delay yang terasa. Solusinya: aktifkan connection pooling di Nodemailer dengan opsi pool: true dan buat transporter sekali saat aplikasi start, bukan di dalam fungsi kirim email.
Hasbi Putra adalah Head of Marketing di KIRIM.EMAIL, email delivery infrastructure untuk developer dan tim IT di Indonesia. KIRIM.EMAIL mengirim lebih dari 11 juta email per hari dengan server yang sepenuhnya berlokasi di Indonesia.
- Kenapa Email Laravel Masuk Spam Padahal SPF Sudah Benar - April 5, 2026
- Cara Kirim OTP via Email di Node.js yang Tidak Delay dan Tidak Masuk Spam - April 3, 2026
- CAN-SPAM untuk Developer: Yang Harus Anda Implementasi di Kode Anda - April 2, 2026
