Diagnosing Goroutine Leaks in Go Fiber Applications
A Go Fiber service can leak goroutines when request handlers never exit. Confirm it with pprof and fix it with timeouts and detached context.
23 Oct 2025, 00:51 UTC

The problem: memory and goroutines keep climbing
Your Go service built with the Fiber web framework uses more memory every hour, even when traffic stops. The process is eventually terminated by an out-of-memory killer. The cause is often not a traditional memory leak, but a goroutine leak: handler goroutines that start with a request and never finish.
Useful takeaway: you can confirm the leak by comparing goroutine counts before and after a load test, then pinpoint the stuck code with pprof. The fix usually means bounding handler execution with a timeout and making sure background work does not outlive the request.
A goroutine is a lightweight execution unit managed by the Go runtime. RSS is the resident set size, or the physical memory currently used by the process. Fiber is a Go web framework built on fasthttp. pprof is Go's built-in profiling tool, exposed through the net/http/pprof package.
Recognizable conditions and likely causes
| Condition | Likely cause | Where to check |
|---|---|---|
| RSS climbs with request volume and never falls during idle | Handler goroutines blocked on downstream calls or locks | runtime.NumGoroutine() |
| Goroutine count stays high after traffic ends | Background goroutines started inside a request and tied to its context | /debug/pprof/goroutine?debug=1 |
| Handlers hang under latency from a database or API | No deadline on external I/O inside the handler | pprof stack traces showing the same blocked function |
Ordered checks
- Establish a baseline. Start the application in a local or staging environment. Log runtime.NumGoroutine() at startup or expose it through a debug endpoint. Record the number while the service is idle.
- Generate representative load. Run a load test against a real endpoint, not just a health check. Run this command from your local terminal; no special permissions are required:
hey -z 120s -q 100 http://localhost:3000/api/usersReplace localhost:3000 with your service address. A load test can affect shared staging environments, so avoid running it against production without approval.
- Observe after the test ends. Wait five to ten minutes while the service is idle. If the goroutine count does not return to the baseline recorded in step one, you have a persistent leak candidate.
- Capture pprof data. Expose the debug endpoint in your Fiber application:
import _ "net/http/pprof" go func() { _ = http.ListenAndServe("localhost:6060", nil) }()Then request the goroutine profile from a terminal on the same network as the service:
curl http://localhost:6060/debug/pprof/goroutine?debug=1Do not expose /debug/pprof on a public network without authentication; it reveals internal stack traces.
- Analyze the stacks. Look for many goroutines blocked in the same function, such as an HTTP client call, a database query, or a channel send. That shared stack frame is the leak location.
Fixes matched to findings
If handlers block on downstream calls
Wrap the handler work in a context with a timeout. This ensures the goroutine exits even if the downstream service does not respond.
app.Get("/api/users", func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://downstream.example.com/users", nil)
if err != nil {
return c.Status(500).SendString(err.Error())
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return c.Status(502).SendString("upstream unavailable")
}
defer resp.Body.Close()
return c.SendStatus(resp.StatusCode)
})
Where to run: in your application source code, then rebuild and redeploy. Required permissions: write access to the repository and deployment pipeline. Expected check: after redeploy, the goroutine count returns to baseline after load stops.
If background goroutines outlive the request
Do not pass the request context to background work unless that work is meant to last for the request lifetime. Use a detached context or a worker pool with its own shutdown signal.
go func() {
<-appCtx.Done()
}()
Risk: if the background goroutine writes to the Fiber response after the client disconnects, the write can block or return an error. Keep response writes inside the request handler only.
If unbuffered channels or locks cause indefinite blocks
Add a buffer, a select with a default case, or a timeout so the handler cannot wait forever.
Escalation criteria
- Goroutine count does not return to baseline within ten minutes after load stops.
- Goroutine count grows by more than 20 percent per hour during steady traffic.
- pprof shows more than 100 goroutines stuck in a single stack frame.
- Memory growth continues after handler timeouts are added.
If any criterion is met, escalate to the team that owns the service and capture a full pprof heap and goroutine profile before restarting the process.
Limitations and practical verification
A high goroutine count is not always a leak. A service that intentionally processes many concurrent jobs may legitimately carry hundreds of goroutines. Compare against baseline, not against an absolute number.
Do not call runtime.GC() manually to force memory down during diagnosis. It can clean reachable but unused memory and delay detection of the real leak.
Practical check: after applying a fix, run the same load test, wait for idle, and confirm runtime.NumGoroutine() matches the pre-test baseline. Version assumptions: Go 1.21 or later and Fiber v2.x. Confirm your exact Fiber release before relying on framework-specific timeout configuration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.