Introduction
While working on a Golang microservice yesterday, I stumbled upon a subtle yet critical bug that caused memory usage to grow uncontrollably over time. After some digging, I realized the root cause was a missing resp.Body.Close() in an infinite loop where HTTP requests were being made. This oversight led to a steady increase in the number of goroutines, ultimately resulting in a memory leak.
Here's what I learned and how I fixed it.
The Problem: Infinite Loop + Missing Cleanup
In our codebase, we had a long-running background goroutine that continuously made HTTP requests to a remote service. Here's a simplified version of the problematic code:
for {
resp, err := http.Get("https://example.com/api/data")
if err != nil {
log.Println("request failed:", err)
continue
}
// Process the response (dummy logic)
process(resp.Body)
// Missing resp.Body.Close()
}
At first glance, the code appears harmless. But there's one critical mistake: we forgot to close the response body. In Go, every successful HTTP request must be followed by resp.Body.Close() to release the connection back to the pool.
Because this was inside an infinite loop, each iteration created a new HTTP response object. Without closing the body, the underlying TCP connections were never released, and goroutines handling those connections began to accumulate.
The Symptom: Memory Bloat and Goroutine Explosion
Over time, we noticed:
Gradual increase in memory usage.
runtime.NumGoroutine() kept increasing without bound.
HTTP client became slower and eventually stalled due to exhausted resources.
Using pprof, we identified a large number of goroutines blocked in net/http.(*persistConn).readLoop, which was a clear indicator that response bodies weren’t being closed.
The Fix: Close the Response Body!
The fix was straightforward once the problem was identified:
for {
resp, err := http.Get("https://example.com/api/data")
if err != nil {
log.Println("request failed:", err)
continue
}
// Always close the response body
func() {
defer resp.Body.Close()
process(resp.Body)
}()
}
We wrapped the processing in an anonymous function and used defer resp.Body.Close() to ensure the body is closed even if the processing fails midway.
Takeaways
Always close resp.Body after you're done with it, especially in loops or background goroutines.
Leaked HTTP connections can lead to goroutine leaks and excessive memory usage.
Tools like pprof and runtime.NumGoroutine() are invaluable for diagnosing such issues.
Be cautious with infinite loops—resource leaks in them scale quickly.
Conclusion
This bug was a subtle reminder of how a small oversight in Go's resource management model can lead to large-scale operational issues. If you're working with net/http, always remember: what you open, you must close.
Top comments (0)