fetch() di bun bisa bikin production mati perlahan

EN

fetch() di bun bisa bikin production mati perlahan

ini cerita beberapa hari debugging fetch timeout di production. kalau kamu jalanin bun 1.3.x di docker dan ada outgoing HTTPS calls, semoga ini ngebantu

konteks awal

API server jalan di docker swarm, multiple replicas di belakang nginx. app-nya bikin outgoing HTTPS ke beberapa external services, ada yang di cloud run, push notification, SMS gateway

awalnya pakai bun 1.2.18 dengan compiled binary (bun build —compile). secara fungsional aman, fetch ke external services gak ada masalah

tapi bun 1.2.18 punya bug GC HeapHelper yang bikin tiap process makan CPU ~28-50% walaupun idle. kalau jalanin 5 replicas berarti 150-250% CPU cuma buat garbage collector doing nothing

akhirnya upgrade ke bun 1.3.13 buat fix GC CPU issue. CPU langsung turun

ini CPU dan memory waktu masih di bun 1.2.18

grafana service CPU dan memory usage dengan bun 1.2.18 menunjukkan API CPU mean 213%

gejalanya

setelah upgrade ke bun 1.3.13, outgoing fetch ke external API mulai timeout di exactly 15 detik. gak selalu, intermittent. kadang 50% request timeout, kadang 0%

yang bikin susah, cuma kejadian under real traffic. semua isolated test aman

perjalanan debug

service pihak ketiga yang lambat

insting pertama, external API-nya yang lambat. tapi curl dari container yang sama? 65-200ms. selalu. jadi bukan service-nya

DNS-nya bermasalah

cari di bun issues, nemu oven-sh/bun#10731 soal c-ares DNS. udah coba:

  • BUN_DNS_RESOLVER=getaddrinfo pas build
  • setDefaultResultOrder(‘ipv4first’) di code
  • disable IPv6 pakai sysctl

DNS dari container aman (1-20ms). gak ada yang fix

compiled binary-nya beda

kita pakai bun build —compile. install bun runtime di container yang sama dan test secara isolated, hasilnya aman. ganti ke runtime mode

tapi under real traffic tetep kejadian

connection pooling-nya bocor

nemu oven-sh/bun#9034 soal keep-alive reuse dead socket. tambahin keepalive: false di semua fetch

gak fix

IPv6-nya bermasalah

nemu cloud run target gak punya AAAA record. test curl IPv6 hasilnya HTTP 000, 1-6 detik. bun coba IPv6 duluan!

disable IPv6, tambahin ipv4first. timeout turun dari 50% ke ~5%. tapi masih ada

perlu HTTP/2

nemu oven-sh/bun#13586, maintainer bun confirm fetch pakai HTTP/1.1, beberapa server respond lebih cepat ke HTTP/2. coba {protocol: “http2”} (experimental di 1.3.14)

isolated test mantap. under load masih timeout. fitur experimental belum stabil

breakthrough

install bun runtime di container dan jalanin comparison test:

// bun fetch()
const r = await fetch(url, { headers })
// vs node:https
const r = await httpsRequest(url, { headers })

keduanya identik di isolated test. tapi under real traffic:

clientisolatedunder load
fetch()65ms15,000ms timeout
node:https55ms55ms
curl65ms65ms

node:https dan curl gak terpengaruh concurrent load sama sekali. cuma bun fetch() yang degradasi

ini outgoing dan incoming latency waktu masalah terjadi:

grafana outgoing latency sebelum fix menunjukkan timeout 15 detik

grafana incoming latency sebelum fix

yang menarik, outgoing mentok 15 detik tapi incoming P99 cuma keliatan ~3-4 detik. padahal request yang trigger outgoing fetch harusnya juga 15 detik. alasannya karena histogram bucket incoming (dari hono-prometheus) cuma sampai 10 detik, setelah itu langsung +Inf. histogram_quantile interpolate antara 10s dan infinity, hasilnya gak akurat. sementara outgoing histogram punya bucket sampai 15 detik (kita define sendiri), jadi bisa nunjukin angka sebenarnya

pattern-nya

degradasinya progresif, bukan tiba-tiba:

  • 0-10 menit: ~100ms (normal)
  • 10-20 menit: ~2-4s (mulai naik)
  • 20-30 menit: ~8-15s (timeout)

dan kena semua external HTTPS yang pakai fetch(). yang gak kena cuma internal HTTP calls dan worker process yang sequential tanpa Bun.serve()

point terakhir itu kunci. bun version sama, fetch() sama, docker container sama, tapi worker (yang proses job sequential, tanpa Bun.serve()) gak pernah kena

root cause

bun 1.3.x punya regresi di implementasi native fetch() saat dipakai buat outgoing HTTPS under concurrent Bun.serve() incoming load. internal TLS connection pool degradasi seiring waktu

gak kejadian di:

  • node:https (networking stack beda di bun)
  • curl (completely separate)
  • bun 1.2.18 (internals fetch beda)
  • sequential processing (tanpa concurrent Bun.serve())

fix-nya

ganti fetch() ke node:https buat external HTTPS calls:

import https from 'node:https'

function httpsRequest(url: string, options: RequestOptions): Promise<HttpResponse> {
  return new Promise((resolve, reject) => {
    const u = new URL(url)
    const req = https.request({
      hostname: u.hostname,
      path: u.pathname + u.search,
      method: options.method || 'GET',
      headers: options.headers || {},
      family: 4,
    }, (res) => {
      let data = ''
      res.on('data', (chunk) => data += chunk)
      res.on('end', () => resolve({ status: res.statusCode || 0, data }))
    })
    req.on('error', reject)
    req.setTimeout(options.timeout || 15000, () => {
      req.destroy(new Error('Request timeout'))
    })
    if (options.body) req.write(options.body)
    req.end()
  })
}

setelah deploy, external API calls dari 15s timeout jadi konsisten 50-200ms. zero degradasi over time

after (pakai node:https):

grafana outgoing latency setelah fix menunjukkan semua service di bawah 1 detik

grafana incoming latency setelah fix menunjukkan P99 turun ke ratusan ms

yang aku pelajarin

  1. isolated test bohong. masalahnya cuma muncul under real concurrent load. mau test bun script.js berapa kalipun gak bakal reproduce

  2. curl dari container buktiin network aman. kalau curl jalan tapi runtime gak, masalahnya di runtime

  3. bandingin worker vs API itu breakthrough. code sama, container sama, concurrency model beda, kalau satu jalan dan satu gak, itu concurrency bug

  4. node:https itu escape hatch di bun. bun implement Node.js API pakai code path beda dari native fetch(). kalau satu rusak, yang lain bisa jalan

  5. monitor outgoing latency terpisah. kita punya prometheus histogram buat outgoing calls. tanpa ini, gak bakal notice degradasi progresifnya

bun issues yang related:

kalau kamu kena masalah serupa, coba node:https dulu sebelum nyalahin infra


← Kembali ke blog