Understanding Context Deadline Exceeded
The context deadline exceeded error appears when an operation doesn't complete within the specified timeout:
context deadline exceeded
context canceled
rpc error: code = DeadlineExceeded desc = context deadline exceededThis is Go's way of enforcing timeouts and cancellations for operations.
Common Scenarios and Solutions
Scenario 1: Database Query Timeout
Problem code: ```go func getUser(db *sql.DB, id int) (*User, error) { ctx := context.Background() query := "SELECT * FROM users WHERE id = $1"
var user User err := db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name) // If query takes too long, context deadline is exceeded return &user, err } ```
Solution - Set appropriate timeout: ```go func getUser(db *sql.DB, id int) (*User, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Always call cancel to release resources
query := "SELECT * FROM users WHERE id = $1"
var user User err := db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, fmt.Errorf("query timeout after 5 seconds") } return nil, err } return &user, nil } ```
Scenario 2: HTTP Request Timeout
Problem code: ```go func fetchData(url string) ([]byte, error) { ctx := context.Background() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
client := &http.Client{} resp, err := client.Do(req) // Could hang indefinitely } ```
Solution - Layer timeout and context: ```go func fetchData(url string, timeout time.Duration) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) }
client := &http.Client{ Timeout: timeout + time.Second, // Client timeout > context timeout }
resp, err := client.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, fmt.Errorf("request timed out after %v", timeout) } return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close()
return io.ReadAll(resp.Body) } ```
Scenario 3: Context Not Propagated
Problem code: ```go func processRequest(w http.ResponseWriter, r *http.Request) { // Ignoring request context ctx := context.Background() // Wrong!
data, err := fetchUserData(ctx, userID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } json.NewEncoder(w).Encode(data) }
// When user cancels request, operation continues running ```
Solution - Propagate request context: ```go func processRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Use request context
// Add timeout if needed ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel()
data, err := fetchUserData(ctx, userID) if err != nil { if errors.Is(err, context.Canceled) { // Client disconnected, no need to respond return } if errors.Is(err, context.DeadlineExceeded) { http.Error(w, "request timeout", http.StatusGatewayTimeout) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(data) } ```
Scenario 4: Nested Context Issues
Problem code: ```go func parent(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel()
return child(ctx) }
func child(ctx context.Context) error { // Creates another timeout, but parent might cancel first ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
// This context is independent - won't be canceled when parent cancels! return doWork(ctx) } ```
Solution - Propagate parent context: ```go func parent(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel()
return child(ctx) // Pass the derived context }
func child(ctx context.Context) error { // Child timeout must be less than parent's remaining time // Or just use parent's context directly return doWork(ctx) }
// Or use a shorter timeout that respects parent func childWithTimeout(ctx context.Context) error { // Use 5 seconds or parent's remaining time, whichever is less ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel()
return doWork(ctx) } ```
Scenario 5: Goroutine Leak from Missing Cancellation
Problem code: ```go func processData(ctx context.Context) error { results := make(chan Result)
go func() { // This goroutine never stops if ctx is canceled for { result := expensiveComputation() results <- result } }()
select { case result := <-results: return process(result) case <-ctx.Done(): return ctx.Err() // Goroutine continues running! } } ```
Solution - Handle context cancellation in goroutine: ```go func processData(ctx context.Context) error { results := make(chan Result)
ctx, cancel := context.WithCancel(ctx) defer cancel() // Cancel when function returns
go func() { defer close(results) for { select { case <-ctx.Done(): return // Stop goroutine when context cancels default: result := expensiveComputation() select { case results <- result: case <-ctx.Done(): return } } } }()
select { case result, ok := <-results: if !ok { return errors.New("no results") } return process(result) case <-ctx.Done(): return ctx.Err() } } ```
Scenario 6: gRPC Timeout
Problem code:
``go
func callRemote(client pb.ServiceClient) (*pb.Response, error) {
ctx := context.Background()
return client.GetData(ctx, &pb.Request{})
// No timeout - could hang forever
}
Solution - Set gRPC timeouts: ```go func callRemote(client pb.ServiceClient) (*pb.Response, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
resp, err := client.GetData(ctx, &pb.Request{}) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, fmt.Errorf("gRPC call timed out") }
// Check gRPC status st, ok := status.FromError(err) if ok { if st.Code() == codes.DeadlineExceeded { return nil, fmt.Errorf("server timeout: %s", st.Message()) } }
return nil, err } return resp, nil } ```
Context Best Practices
Pattern 1: Timeout Hierarchy
```go func handleRequest(w http.ResponseWriter, r *http.Request) { // Overall request timeout: 30 seconds ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel()
// Database query: max 10 seconds dbCtx, dbCancel := context.WithTimeout(ctx, 10*time.Second) user, err := getUser(dbCtx, userID) dbCancel()
// External API: max 5 seconds apiCtx, apiCancel := context.WithTimeout(ctx, 5*time.Second) data, err := fetchExternalData(apiCtx, url) apiCancel() } ```
Pattern 2: Context Values for Request Tracing
```go type contextKey string
const ( requestIDKey contextKey = "requestID" userIDKey contextKey = "userID" )
func withRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id) }
func getTraceLogger(ctx context.Context) *log.Logger { requestID, _ := ctx.Value(requestIDKey).(string) return log.New(os.Stdout, fmt.Sprintf("[%s] ", requestID), log.LstdFlags) } ```
Pattern 3: Graceful Shutdown
```go func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel()
// Handle shutdown signals sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() { <-sigCh fmt.Println("Shutdown signal received...") cancel() }()
// Worker respects context var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() worker(ctx) }()
wg.Wait() fmt.Println("Graceful shutdown complete") }
func worker(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("Worker stopping...") return default: // Do work process(ctx) } } } ```
Debugging Context Issues
```go // Add context inspection func inspectContext(ctx context.Context) { deadline, hasDeadline := ctx.Deadline() fmt.Printf("Has deadline: %v\n", hasDeadline) if hasDeadline { fmt.Printf("Deadline: %v (in %v)\n", deadline, time.Until(deadline)) }
err := ctx.Err() if err != nil { fmt.Printf("Context error: %v\n", err) } } ```
Verification
```go func TestContextTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel()
done := make(chan bool) go func() { time.Sleep(200 * time.Millisecond) // Longer than timeout done <- true }()
select { case <-done: t.Error("should have timed out") case <-ctx.Done(): if !errors.Is(ctx.Err(), context.DeadlineExceeded) { t.Errorf("expected DeadlineExceeded, got %v", ctx.Err()) } } } ```