Understanding Context Deadline Exceeded

The context deadline exceeded error appears when an operation doesn't complete within the specified timeout:

bash
context deadline exceeded
context canceled
rpc error: code = DeadlineExceeded desc = context deadline exceeded

This 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()) } } } ```