Fetch Node.js Kadang Lambat: Cara Membuktikan Masalah DNS
Prosedur membedakan latency DNS, TCP, TLS, dan upstream pada fetch Node.js tanpa menebak atau menonaktifkan IPv6 secara buta.
0xNN · · 9 min read
Fetch Node.js Kadang Lambat: Cara Membuktikan Apakah DNS Penyebabnya
Request HTTP yang sesekali lambat tidak otomatis berarti API tujuan sedang bermasalah. Waktu total dapat habis pada beberapa tahap: antrean client, resolusi nama, pembukaan koneksi TCP, TLS, menunggu byte pertama, atau mengunduh body.
DNS adalah salah satu kandidat, tetapi diagnosis yang baik harus memisahkan setiap tahap sebelum mengubah konfigurasi jaringan.
---
dns.lookup() dan dns.resolve*() Bukan Hal yang Sama
Node.js menyediakan dua keluarga API:
• dns.lookup() memakai fasilitas resolver sistem operasi seperti getaddrinfo. Hasilnya mengikuti konfigurasi host, misalnya /etc/hosts, nsswitch.conf, dan kebijakan urutan alamat;
• dns.resolve4(), dns.resolve6(), dan fungsi resolve* lain melakukan kueri DNS melalui jaringan dan tidak memakai /etc/hosts.
Walaupun callback dns.lookup() bersifat asynchronous dari sisi JavaScript, implementasinya memakai panggilan sinkron getaddrinfo di libuv thread pool. Tekanan pada thread pool dapat ikut memengaruhi latensi lookup.
fetch() bawaan Node menggunakan Undici. Jangan berasumsi ia selalu memanggil dns.resolve() atau selalu mencoba IPv6 lebih dulu; perilaku koneksi bergantung pada versi Node/Undici, resolver OS, alamat yang tersedia, dan opsi connector.
---
Ukur DNS Secara Terpisah
Mulai dengan script kecil:
import { lookup, resolve4, resolve6 } from 'node:dns/promises';
import { performance } from 'node:perf_hooks';
const hostname = process.argv[2] ?? 'example.com';
async function timed(label, operation) {
const start = performance.now();
try {
const value = await operation();
console.log(label, {
durationMs: Number((performance.now() - start).toFixed(1)),
value,
});
} catch (error) {
console.error(label, {
durationMs: Number((performance.now() - start).toFixed(1)),
code: error.code,
message: error.message,
});
}
}
await timed('lookup-all', () => lookup(hostname, { all: true }));
await timed('resolve4', () => resolve4(hostname));
await timed('resolve6', () => resolve6(hostname));
Jalankan beberapa kali dari host production atau replika jaringan yang sama:
node dns-check.mjs api.example.com
Interpretasi:
• lookup lambat tetapi resolve4/resolve6 cepat: periksa resolver OS, /etc/hosts, NSS, dan kepadatan libuv thread pool;
• semua resolver lambat: periksa recursive resolver, packet loss, firewall UDP/TCP 53, atau jalur jaringan;
• resolver cepat tetapi fetch lambat: masalah kemungkinan berada pada koneksi, TLS, server tujuan, antrean socket, atau response;
• resolve6 menghasilkan alamat tetapi koneksi IPv6 gagal: host mungkin memiliki jalur IPv6 yang tidak berfungsi penuh.
Satu eksekusi tidak cukup karena cache DNS dapat membuat hasil berikutnya jauh lebih cepat.
---
Pisahkan Waktu Request dengan curl
curl dapat melaporkan tahapan request:
curl -sS -o /dev/null \
-w 'namelookup=%{time_namelookup}s connect=%{time_connect}s appconnect=%{time_appconnect}s starttransfer=%{time_starttransfer}s total=%{time_total}s\n' \
https://api.example.com/health
Maknanya:
• time_namelookup: resolusi nama;
• time_connect: koneksi TCP selesai;
• time_appconnect: TLS selesai;
• time_starttransfer: byte pertama diterima;
• time_total: seluruh transfer selesai.
Hasil curl dan Node tidak harus sama karena resolver, connection reuse, dan implementasi HTTP bisa berbeda. Gunakan keduanya untuk mempersempit tahap, bukan untuk menyimpulkan salah satu client “rusak”.
---
Periksa IPv4 dan IPv6 Tanpa Menebak
Bandingkan jalur secara eksplisit:
curl -4 -sS -o /dev/null -w 'ipv4 total=%{time_total}s\n' https://api.example.com/health
curl -6 -sS -o /dev/null -w 'ipv6 total=%{time_total}s\n' https://api.example.com/health
Jika IPv6 gagal sementara DNS mengembalikan AAAA, perbaikan terbaik adalah memperbaiki routing, security group, firewall, atau DNS record yang tidak dapat dilayani. Menghapus dukungan IPv6 secara global hanya untuk menutupi satu endpoint dapat mengubah perilaku semua aplikasi di host.
Node menyediakan kontrol urutan hasil lookup:
import dns from 'node:dns';
dns.setDefaultResultOrder('ipv4first');
Ini dapat menjadi mitigasi terbatas setelah pengujian, bukan langkah pertama. Pengaturan tersebut memengaruhi urutan hasil dns.lookup() pada thread tempat ia dipanggil; ia tidak memperbaiki jaringan IPv6 dan tidak mengubah keluarga dns.resolve*().
Pada versi Node yang mendukungnya, API koneksi juga memiliki opsi family autoselection. Periksa dokumentasi versi runtime yang benar sebelum mengubahnya.
---
Jangan Mengganti Hostname dengan IP Secara Naif
Kode seperti ini bermasalah:
// Hindari pola ini
const response = await fetch('https://203.0.113.10/v1/price');
TLS memvalidasi sertifikat terhadap hostname dan virtual hosting memakai SNI/Host. Mengganti hostname dengan IP dapat menyebabkan certificate mismatch, mengarah ke virtual host yang salah, dan melewati perubahan alamat dari penyedia.
Jika membutuhkan DNS cache atau custom lookup, gunakan fasilitas connector/agent HTTP yang tetap mempertahankan hostname untuk TLS, serta hormati TTL dan rotasi alamat. Jangan membuat cache tanpa expiry.
---
Timeout dan Error Harus Terlihat
Batasi request dan simpan konteks error:
async function fetchHealth(url) {
const startedAt = performance.now();
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
throw new Error(upstream returned ${response.status});
}
return await response.json();
} catch (error) {
console.error('upstream request failed', {
hostname: new URL(url).hostname,
durationMs: Math.round(performance.now() - startedAt),
name: error.name,
causeCode: error.cause?.code,
});
throw error;
}
}
Jangan log API key, authorization header, query sensitif, atau response body mentah. Pada sistem yang lebih besar, gunakan telemetry HTTP/DNS dan histogram latency agar outlier terlihat tanpa mengandalkan log per request.
---
Connection Reuse Dapat Menyamarkan Hasil
HTTP keep-alive membuat request berikutnya memakai socket yang sudah terbuka, sehingga tidak melakukan DNS, TCP, dan TLS lagi. Ini baik untuk production, tetapi dapat membuat eksperimen DNS tampak “sembuh” setelah request pertama.
Saat menguji, bedakan cold connection dan reused connection. Catat apakah client memakai pool socket, berapa lama socket idle dipertahankan, dan apakah target mengirim Connection: close. Jangan mematikan keep-alive sebagai solusi DNS; biasanya itu justru menambah jumlah lookup dan handshake.
Periksa juga apakah beberapa hostname mengarah ke CDN atau load balancer dengan banyak alamat. Satu alamat atau region yang bermasalah dapat menghasilkan latency intermiten meskipun lookup cepat. Simpan alamat remote dan timing secara aman pada telemetry untuk melihat korelasi, tanpa hard-code satu IP sebagai perbaikan permanen.
---
Checklist Diagnosis
1. Ukur lookup, TCP, TLS, TTFB, dan total secara terpisah.
2. Ulangi dari jaringan production, bukan hanya laptop.
3. Bandingkan A/AAAA dan konektivitas IPv4/IPv6.
4. Periksa versi Node dan perilaku connector yang digunakan.
5. Periksa thread-pool pressure jika dns.lookup() yang lambat.
6. Terapkan mitigasi paling sempit, lalu ukur kembali.
7. Hindari disable IPv6 global atau hard-code IP tanpa memahami TLS dan TTL.
Kesimpulan “ini DNS” baru layak dibuat jika waktu lookup atau percobaan alamat memang terbukti menjadi bagian yang lambat.
Referensi
• Node.js: DNS
• Node.js: dns.setDefaultResultOrder()
• Node.js: net.connect() family autoselection
• Undici: Dispatcher API