Timeout & batas server
Default net/http adalah "tidak ada batas": tidak ada batas waktu membaca, menulis, maupun menganggur. Untuk contoh di tutorial itu praktis; untuk server yang terbuka ke internet itu kerentanan.
Intisari
ReadHeaderTimeoutadalah pertahanan terhadap Slowloris โ ribuan koneksi yang mengirim header satu byte per menit.WriteTimeoutdihitung sejak header permintaan selesai dibaca, jadi ia mencakup seluruh waktu handler-mu.IdleTimeoutmengatur berapa lama koneksi keep-alive dipertahankan; tanpa itu ia mewarisiReadTimeout.- Timeout server adalah jaring pengaman; kendali sebenarnya ada di
contextper permintaan. - Unggahan berkas besar dan SSE butuh pengecualian โ pakai
http.ResponseController, bukan menaikkan timeout global.
Konfigurasi yang layak produksi
srv := &http.Server{
Addr: ":8080",
Handler: handler,
// Waktu maksimum membaca HEADER. Pertahanan Slowloris.
ReadHeaderTimeout: 5 * time.Second,
// Header + body. Naikkan hanya kalau memang menerima unggahan besar.
ReadTimeout: 15 * time.Second,
// Dari header selesai dibaca sampai respons selesai ditulis.
// Ini mencakup SELURUH waktu kerja handler-mu.
WriteTimeout: 15 * time.Second,
// Berapa lama koneksi keep-alive menganggur sebelum ditutup.
// Harus LEBIH BESAR dari idle timeout ALB (Fase 10).
IdleTimeout: 65 * time.Second,
// Batas ukuran seluruh header. Default 1 MB.
MaxHeaderBytes: 1 << 20,
}
| Field | Dihitung dari | Sampai | Default |
|---|---|---|---|
ReadHeaderTimeout | Koneksi diterima | Header selesai dibaca | tak terbatas |
ReadTimeout | Koneksi diterima | Body selesai dibaca | tak terbatas |
WriteTimeout | Header selesai dibaca | Respons selesai ditulis | tak terbatas |
IdleTimeout | Respons selesai | Permintaan berikutnya | Ikut ReadTimeout |
Serangan yang dicegahnya
Slowloris: buka 10.000 koneksi, kirim
"GET / HTTP/1.1\r\n"
lalu satu header tiap 30 detik, tanpa pernah selesai.
Tanpa ReadHeaderTimeout:
โข 10.000 goroutine hidup, masing-masing menunggu selamanya
โข 10.000 file descriptor terpakai
โข memori naik, pengguna sungguhan tidak kebagian koneksi
โข satu laptop cukup untuk melakukannya
ReadHeaderTimeout adalah satu baris dengan rasio manfaat tertinggi di seluruh
konfigurasi server Go. Linter gosec (aturan G112) menandai server tanpa field ini
sebagai temuan keamanan โ pasang golangci-lint di Fase 8 dan ia akan mengingatkanmu.
Kenapa WriteTimeout sering menipu
// Handler ini butuh 20 detik. Dengan WriteTimeout 15 detik:
// koneksi diputus di detik 15, dan KLIEN menerima respons terpotong โ
// sementara handler-mu TETAP BERJALAN sampai selesai.
func lambat(w http.ResponseWriter, r *http.Request) {
hasil := prosesBerat(r.Context()) // 20 detik
json.NewEncoder(w).Encode(hasil) // koneksi sudah mati
}
Timeout server memutus koneksi; ia tidak menghentikan handler. Yang menghentikan handler adalah context. Karena itu keduanya dipakai bersama:
// Middleware timeout memasang tenggat DI CONTEXT โ inilah yang benar-benar
// menghentikan pekerjaan, termasuk kueri database di lapisan terdalam.
func BatasWaktu(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, batal := context.WithTimeout(r.Context(), d)
defer batal()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
| Timeout server | Timeout context | |
|---|---|---|
| Yang dihentikan | Koneksi TCP | Pekerjaan di dalam handler |
| Berlaku untuk | Semua permintaan, seragam | Bisa berbeda per rute |
| Handler tahu? | Tidak | Ya โ lewat ctx.Done() |
| Perannya | Jaring pengaman terakhir | Kendali sebenarnya |
Endpoint yang perlu pengecualian
// Unggahan besar atau SSE: perpanjang tenggat per permintaan,
// tanpa melonggarkan seluruh server.
func unggah(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w) // Go 1.20+
_ = rc.SetReadDeadline(time.Now().Add(10 * time.Minute))
_ = rc.SetWriteDeadline(time.Time{}) // tanpa batas tulis
...
}
// Server-sent events: matikan batas tulis dan flush setiap pesan.
func peristiwa(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
_ = rc.SetWriteDeadline(time.Time{})
w.Header().Set("Content-Type", "text/event-stream")
for {
select {
case <-r.Context().Done():
return
case p := <-pesan:
fmt.Fprintf(w, "data: %s\n\n", p)
_ = rc.Flush()
}
}
}
http.ResponseController menggantikan trik lama berupa type assertion ke
http.Flusher dan menyetel WriteTimeout: 0 untuk seluruh server. Sebelumnya,
satu endpoint streaming memaksa seluruh server berjalan tanpa batas tulis โ kompromi yang tidak perlu
lagi diambil.
Menyelaraskan dengan ALB
| Setelan | Nilai | Kenapa |
|---|---|---|
| ALB idle timeout | 60 dtk | Default AWS |
Go IdleTimeout | 65 dtk | Harus lebih besar, kalau tidak akan ada 502 sesekali |
| ALB deregistration delay | 30 dtk | Harus โฅ waktu graceful shutdown |
502 acak yang sulit dilacak hampir selalu ini penyebabnya. Kalau server Go menutup koneksi keep-alive lebih dulu daripada ALB menyadarinya, ALB bisa mengirim permintaan ke koneksi yang sedang ditutup โ dan hasilnya 502 untuk pengguna sungguhan, dengan log aplikasi yang bersih total. Aturannya satu kalimat: IdleTimeout aplikasi harus lebih besar daripada idle timeout load balancer.
Latihan: jalankan server tanpa ReadHeaderTimeout, lalu buka koneksi dengan
telnet localhost 8080, ketik GET / HTTP/1.1 dan tekan Enter sekali โ lalu
diamkan. Cek runtime.NumGoroutine() lewat endpoint pprof dan lihat goroutine itu bertahan.
Pasang ReadHeaderTimeout: 5 * time.Second dan ulangi.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.