GO HTTP SERVER BEST PRACTİCES: TİMEOUT, MİDDLEWARE VE GRACEFUL SHUTDOWN
Go ile HTTP sunucusu yazmak inanılmaz kolaydır; birkaç satır kodla çalışan bir servis ayağa kalkar. Asıl mesele, bu servisin üretim ortamında da aynı “kolaylıkla” ayakta kalmasıdır: ağır trafik, yavaş istemciler, ters proxy arkasında çalışma, ani yeniden başlatmalar ve beklenmedik hatalar...
Bu yazıda Go HTTP server best practices bakış açısıyla üç temel başlığı merkeze alacağız: doğru timeout stratejileri, ölçeklenebilir bir middleware mimarisi ve dağıtım/yeniden başlatma süreçlerinde servis kalitesini koruyan graceful shutdown.
Hedefimiz “çalışan” kod değil, sürdürülebilir bir servis: kaynak tüketimi kontrol edilen, gözlemlenebilir, güvenlik ve dayanıklılık katmanları olan ve operasyonda sorun çıkarmayan bir HTTP sunucusu.

Primary yaklaşım: Go HTTP server en iyi uygulamalarını netleştirmek
Birincil odağımız, Go’nun net/http paketini kullanırken üretim gereksinimlerini karşılayan ayar ve desenleri standartlaştırmaktır. “En iyi uygulamalar” burada dogma değil; servisinizin SLA/SLO hedeflerine, trafiğin karakterine ve altyapı gerçeklerine göre şekillenen pratiklerdir.
Yine de çoğu sistemde tekrar eden bazı problem alanları vardır: yavaş bağlantılar, sonsuz bekleyen istekler, kontrolsüz goroutine birikimi, log gürültüsü, hatalı retry davranışları ve güvenlik açıkları. Bu yazı, bu riskleri azaltan yapı taşlarını bir araya getirir.
Üretim sinyalleri: SLO, p95/p99 ve kuyruk etkisi
Timeout ve kapasite kararları, tek tek isteklerin ortalama süresinden çok kuyruklanma davranışıyla ilgilidir. p95/p99 gecikmeler yükseldiğinde, yavaş istekler hızlıları da boğar. Bu yüzden, sunucu tarafında sınırları net tanımlamak ve yük altında zarifçe bozulmak (graceful degradation) esastır.
Timeout stratejileri: Read/Write/Idle ve Handler seviyesinde sınırlar
Go HTTP sunucularında timeout’lar çoğu zaman “sonradan eklenen” bir detay gibi görülür. Oysa timeout, kaynak kullanımını ve servis dayanıklılığını doğrudan belirler. Doğru ayarlarla yavaş istemcilerden kaynaklı socket işgali azalır, bekleyen goroutine sayısı kontrol altında kalır ve upstream çağrılarınızın sürünmesi engellenir.
Server timeout’ları: ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout
http.Server üzerinde tanımlanan timeout’lar, bağlantı düzeyinde koruma sağlar. Pratik bir yaklaşım: ReadHeaderTimeout ile header aşamasını sıkı tutmak, IdleTimeout ile keep-alive bağlantıların sonsuza kadar açık kalmasını engellemek, WriteTimeout ile yanıt yazımını sınırlamak.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
// Handler seviyesinde context deadline'a saygı duyun.
select {
case <-time.After(120 * time.Millisecond):
_, _ = w.Write([]byte("done"))
case <-r.Context().Done():
http.Error(w, "request canceled", http.StatusRequestTimeout)
}
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Println("listening on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
// Graceful shutdown sinyal yakalama
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Println("shutting down...")
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
log.Println("bye")
}Handler timeout’ları: context deadline ve upstream çağrıları
Bağlantı seviyesindeki timeout’lar tek başına yeterli değildir. Handler içinde yaptığınız DB/HTTP gibi upstream çağrılar, request context’inden deadline/cancel sinyali almalı. Bu sayede istemci vazgeçtiğinde veya servis limitiniz dolduğunda gereksiz çalışma durur. context kullanımı Go ekosisteminde “opsiyonel” değil, üretim standardıdır.
Middleware zinciri: Logging, Recovery, Request ID, Rate Limiting
Middleware, cross-cutting concern’leri (gözlemlenebilirlik, güvenlik, hata yönetimi) handler’lardan ayrıştırmanın en temiz yoludur. İyi bir middleware zinciri, kod tekrarını azaltırken sistem davranışını tutarlı hale getirir. Buradaki kritik nokta: sırayı doğru kurmak ve gereksiz maliyet yaratmamak.

Önerilen sıra: Request ID → Logging → Recovery → Security → Business
Önce request’e kimlik verin, sonra loglamayı bu kimlikle yapın. Recovery, panik durumunda servisinizin çökmesini engeller; ancak hatayı “yutmak” yerine anlamlı bir 500 yanıtı ve kayıt üretmelidir. Güvenlik katmanı (CORS, header’lar, basit rate limiting) iş mantığından önce konumlanmalıdır.
Basit ama etkili: net/http ile middleware kompozisyonu
Aşağıdaki örnekte, request id, erişim logu ve panic recovery birlikte çalışır. Log formatı ve alan seti, ileride tracing/metrics eklediğinizde genişletilebilir olmalı. Bu, “bugün yeter” ile “yarın sürdürülebilir” arasındaki farktır.
package middleware
import (
"context"
"crypto/rand"
"encoding/hex"
"log"
"net/http"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "req_id"
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b := make([]byte, 8)
_, _ = rand.Read(b)
rid := hex.EncodeToString(b)
ctx := context.WithValue(r.Context(), requestIDKey, rid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.status = code
sw.ResponseWriter.WriteHeader(code)
}
func accessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(sw, r)
rid, _ := r.Context().Value(requestIDKey).(string)
log.Printf("rid=%s method=%s path=%s status=%d dur=%s",
rid, r.Method, r.URL.Path, sw.status, time.Since(start))
})
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
rid, _ := r.Context().Value(requestIDKey).(string)
log.Printf("rid=%s panic=%v", rid, rec)
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func Chain(h http.Handler) http.Handler {
return withRequestID(accessLog(recovery(h)))
}Graceful shutdown: bağlantıları koru, işleri tamamla, süreyi yönet
Graceful shutdown, servisinizin “kapanırken” bile kullanıcı deneyimini korumasıdır. Kubernetes gibi ortamlarda pod’lar yeniden planlandığında veya deploy sırasında, aktif isteklerin yarıda kesilmesi ciddi bir hata dalgasına dönüşebilir. graceful shutdown Go yaklaşımı; sinyal yakalama, yeni bağlantıları durdurma ve aktif işleri süre sınırıyla tamamlamaya dayanır.
Shutdown vs Close: farkı bilin
Server.Close() bağlantıları aniden kapatır; aktif istekler yarıda kalabilir. Server.Shutdown(ctx) ise yeni kabulü durdurur ve mevcut isteklerin tamamlanmasını bekler. Bu bekleme süresini context timeout ile sınırlandırmak, kapanışın uzayıp deploy sürecinizi kilitlemesini engeller.
Arka plan işleri: worker’lar ve context ile kapanış protokolü
HTTP handler’ınızın tetiklediği arka plan işleri varsa, shutdown sırasında bu işleri de durdurmak gerekir. En iyi pratik: bu işleri bir root context üzerinden kontrol etmek, kapanış sinyali geldiğinde cancel etmek ve “in-flight” işleri bir WaitGroup benzeri mekanizma ile sınırlı süre beklemek.
Bağlantı ve kaynak yönetimi: Keep-Alive, MaxHeaderBytes, aşırı yük koruması
Üretimde sürprizler genellikle sınırların tanımlanmadığı yerden gelir. Header boyutu, istek gövdesi, eş zamanlı bağlantı sayısı, yavaş okuyan istemciler… Bunlar için katmanlı bir savunma yaklaşımı kurmak gerekir.
MaxHeaderBytes ve body limitleri
MaxHeaderBytes ile kontrolsüz header şişmesini engelleyebilirsiniz. Body tarafında ise handler içinde http.MaxBytesReader kullanmak iyi bir alışkanlıktır. Böylece “küçük bir endpoint” beklerken devasa payload’lar sunucunuzu zorlamaz.
Reverse proxy arkasında çalışma: timeouts ve gerçek IP
Nginx/Envoy/ALB gibi ters proxy arkasında servis verirken timeout’lar iki tarafta da uyumlu olmalı. Proxy’nin upstream timeout’u, uygulamanın WriteTimeout değerinden daha kısa olursa, uygulama yanıt yazarken bağlantı kopabilir. Ayrıca gerçek istemci IP’si için X-Forwarded-For gibi header’ları güvenli biçimde işlemek gerekir; yalnızca güvenilir proxy’lerden geldiğinde kabul edin.
Gözlemlenebilirlik: yapılandırılmış log, metrik ve hata sınıflandırması
Operasyonda en hızlı iyileştirme kaldıraçlarından biri, doğru sinyalleri üretmektir. “Her şeyi logla” yaklaşımı pahalı ve gürültülüdür; bunun yerine olayları sınıflandırın: 4xx istemci hataları, 5xx sunucu hataları, timeout/cancel gibi kesintiler ve performans istatistikleri.
Request ID ve korelasyon
Her isteğe bir kimlik verip bunu loglara eklemek, dağıtık sistemlerde teşhis süresini dramatik biçimde azaltır. Bir adım ötesi, tracing altyapısıyla (OpenTelemetry gibi) entegrasyondur; ama temel korelasyon bile çoğu ekip için yeterli bir başlangıçtır.
Health check ve readiness: doğru endpoint tasarımı
/healthz gibi basit bir liveness endpoint’i ile servis “çalışıyor mu” sinyalini verin. Readiness için (ör. /readyz) DB bağlantısı veya kritik bağımlılıkların durumunu kontrol etmek mantıklıdır. Böylece trafik, henüz hazır olmayan instance’lara yönlendirilmez.
Güvenlik ve stabilite: CORS, güvenli header’lar ve basit rate limiting
Çoğu API servisi, en azından temel güvenlik header’larına ve isteğe bağlı CORS politikasına ihtiyaç duyar. Rate limiting ise hem kötü niyetli trafiği hem de hatalı istemci döngülerini kontrol altına alır. Bu yazının kapsamı gereği “minimal” bir çerçeve çiziyoruz: karmaşık senaryolarda gateway veya servis mesh desteği daha uygundur.
Pratik kontrol listesi
- Timeout değerleri: header okuma, okuma, yazma ve idle için ayrı ayrı belirle
- Handler içinde context deadline kontrolü ve iptal durumuna saygı
- Recovery middleware ile paniklerin kontrollü 500’e dönmesi
- Request ID + erişim logu ile korelasyon
- Graceful shutdown ile deploy sırasında yarım kalan istekleri azaltma
- Header/body limitleri ve ters proxy uyumu
Örnek mimari akış: “küçük ama üretime hazır” bir net/http servisi
Tüm parçaları birleştirdiğinizde ortaya çıkan resim şudur: HTTP server, bağlantı düzeyinde timeout ve limitlerle korunur; handler’lar context ile sınırlandırılır; middleware zinciri gözlemlenebilirlik ve dayanıklılık sağlar; graceful shutdown ise işletim döngüsünü güvenli hale getirir.

Ne zaman framework, ne zaman net/http?
Bir framework (Gin, Echo vb.) hız kazandırabilir; ancak üretim için gerekli pratikler framework’ten bağımsızdır. Timeout’lar, middleware sırası, shutdown protokolü ve gözlemlenebilirlik yine sizin sorumluluğunuzdadır. Bu yüzden burada anlattıklarımızı hangi framework’ü kullanırsanız kullanın uyarlayabilirsiniz.
Eğitimle derinleşmek isteyenler için
Eğer bu konuları uçtan uca, gerçek senaryolar ve daha kapsamlı örneklerle çalışmak isterseniz Go Eğitimi içeriğine göz atabilirsiniz. Üretim odaklı yaklaşım, sadece kodu değil, işletim biçimini de standarda bağlar.
Sonuç: dayanıklı Go HTTP servisleri için küçük adımlar, büyük farklar
Go’da HTTP sunucusu yazmak kolay; onu “iyi” yapmak ise bilinçli sınırlar ve operasyon odaklı düşünme gerektirir. Timeout’lar, middleware ve graceful shutdown bir araya geldiğinde servisiniz daha öngörülebilir olur: kaynakları daha iyi kullanır, yük altında daha kontrollü davranır ve deploy sırasında daha az hata üretir.
Başlangıç için en iyi adım: mevcut servisinizde timeout’ları gözden geçirmek, bir recovery + request id + access log middleware zinciri kurmak ve shutdown davranışını doğrulamaktır. Bu üçlü, çoğu ekibin üretimde en sık yaşadığı sorunların önemli bir kısmını tek seferde iyileştirir.


