Fix RequestTimeout: context deadline exceeded in Gin
This error occurs when a request handler exceeds the context deadline, typically because a database query or external API call takes too long. Fix it by setting timeouts on your HTTP server, using context.WithTimeout for downstream calls, and returning a 504 Gateway Timeout response when the deadline is exceeded.
Reading the Stack Trace
Here's what each line means:
- main.GenerateReport(0x14000226000) /app/handlers/report.go:22 +0x2d8: The GenerateReport handler at line 22 runs a slow database query that exceeds the request context deadline.
- context.(*cancelCtx).Err(0x14000196040): The context's Err() method returns DeadlineExceeded, indicating the timeout was reached.
- database/sql.(*DB).QueryContext(0x14000118300, {0x1029f0ea0, 0x14000196040}, {0x1028f1e60, 0x2a}, {0x0, 0x0, 0x0}): The database query respects the context deadline and cancels the query when the timeout expires.
Common Causes
1. No timeout on HTTP server or handler
The Gin server has no read/write timeout, allowing requests to hang indefinitely.
func main() {
r := gin.Default()
r.GET("/api/reports", GenerateReport)
r.Run(":8080") // no timeout configured
}
2. Slow database query without context timeout
A complex query runs without a context deadline, blocking the handler until the database responds or the connection times out.
func GenerateReport(c *gin.Context) {
rows, err := db.Query("SELECT * FROM large_table JOIN ...") // no context
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// process rows...
}
3. External API call without timeout
An HTTP call to a third-party service hangs because no timeout is set on the HTTP client.
func GenerateReport(c *gin.Context) {
resp, err := http.Get("https://slow-api.example.com/data") // default client, no timeout
// ...
}
The Fix
Configure HTTP server timeouts and use context.WithTimeout for downstream calls. Check for context.DeadlineExceeded to return a meaningful 504 response instead of a generic 500. Pass the request context to database queries so they cancel when the client disconnects.
func main() {
r := gin.Default()
r.GET("/api/reports", GenerateReport)
r.Run(":8080")
}
func GenerateReport(c *gin.Context) {
rows, err := db.Query("SELECT * FROM large_table")
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
defer rows.Close()
// process rows...
}
func main() {
r := gin.Default()
r.GET("/api/reports", GenerateReport)
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
func GenerateReport(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM large_table")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "request timed out"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer rows.Close()
// process rows...
}
Testing the Fix
package handlers_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestGenerateReport_Timeout(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/reports", func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Millisecond)
defer cancel()
<-ctx.Done()
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "request timed out"})
})
req := httptest.NewRequest(http.MethodGet, "/api/reports", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusGatewayTimeout, w.Code)
assert.Contains(t, w.Body.String(), "timed out")
}
func TestGenerateReport_Success(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/reports", GenerateReport)
req := httptest.NewRequest(http.MethodGet, "/api/reports", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
Run your tests:
go test ./handlers/... -v
Pushing Through CI/CD
git checkout -b fix/gin-timeout-error,git add main.go handlers/report.go,git commit -m "fix: add server and handler timeouts with proper 504 responses",git push origin fix/gin-timeout-error
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go mod download
- run: go vet ./...
- run: go test ./... -race -coverprofile=coverage.out
- run: go build ./...
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
go get github.com/bugstack/sdk
Step 2: Initialize
import "github.com/bugstack/sdk"
func init() {
bugstack.Init(os.Getenv("BUGSTACK_API_KEY"))
}
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run go test ./... locally to confirm timeout handling works.
- Open a pull request with the timeout configuration changes.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify the deployment in staging before promoting to production.
Frequently Asked Questions
BugStack runs the fix through your test suite, generates timeout-specific tests, and validates that slow handlers return proper 504 responses before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Set ReadTimeout to 5-10s, WriteTimeout to 10-30s depending on your slowest endpoint. Use per-handler context timeouts for fine-grained control.
Use an async pattern: accept the request, return 202 Accepted with a job ID, and let the client poll a status endpoint. Process the work in a background goroutine.