Introduction

The context.DeadlineExceeded error occurs when an HTTP request takes longer than the configured timeout. Go's http.Client uses context to propagate cancellation, and when the deadline passes, the request is aborted with this error.

This commonly appears when calling slow external APIs, when network latency spikes, or when timeout values are not configured appropriately for the expected response time.

Symptoms

  • HTTP requests fail with "context deadline exceeded" after a fixed duration
  • Error occurs intermittently under load but not during testing
  • Upstream service responds slowly during peak hours causing timeout cascade

Common Causes

  • http.Client timeout is set too low for the expected response time
  • No timeout configured on http.Client, leading to default behavior with context deadline
  • Upstream service degradation causing response times to exceed threshold

Step-by-Step Fix

  1. 1.Configure appropriate timeout on http.Client: Set explicit timeouts for different phases of the request.
  2. 2.```go
  3. 3.client := &http.Client{
  4. 4.Timeout: 30 * time.Second, // Overall timeout
  5. 5.Transport: &http.Transport{
  6. 6.DialContext: (&net.Dialer{
  7. 7.Timeout: 5 * time.Second, // Connection timeout
  8. 8.KeepAlive: 30 * time.Second,
  9. 9.}).DialContext,
  10. 10.TLSHandshakeTimeout: 5 * time.Second,
  11. 11.ResponseHeaderTimeout: 10 * time.Second,
  12. 12.ExpectContinueTimeout: 1 * time.Second,
  13. 13.MaxIdleConns: 100,
  14. 14.MaxIdleConnsPerHost: 10,
  15. 15.IdleConnTimeout: 90 * time.Second,
  16. 16.},
  17. 17.}

resp, err := client.Get("https://api.example.com/slow-endpoint") ```

  1. 1.Use context.WithTimeout for per-request deadlines: Set different deadlines for different requests.
  2. 2.```go
  3. 3.func fetchData(ctx context.Context, url string) ([]byte, error) {
  4. 4.reqCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
  5. 5.defer cancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) }

resp, err := http.DefaultClient.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, fmt.Errorf("request to %s timed out: %w", url, err) } return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close()

return io.ReadAll(resp.Body) } ```

  1. 1.Implement retry with backoff for transient timeouts: Retry timed-out requests with exponential backoff.
  2. 2.```go
  3. 3.func fetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
  4. 4.var lastErr error
  5. 5.for attempt := 0; attempt < maxRetries; attempt++ {
  6. 6.data, err := fetchData(ctx, url)
  7. 7.if err == nil {
  8. 8.return data, nil
  9. 9.}
  10. 10.lastErr = err

if errors.Is(err, context.DeadlineExceeded) { backoff := time.Duration(math.Pow(2, float64(attempt))) * time.Second select { case <-time.After(backoff): continue case <-ctx.Done(): return nil, ctx.Err() } } return nil, err } return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr) } ```

  1. 1.Add circuit breaker to prevent cascade failures: Stop calling a degraded service.
  2. 2.```go
  3. 3.import "github.com/sony/gobreaker"

var breaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: "api-service", MaxRequests: 3, Interval: 30 * time.Second, Timeout: 10 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures > 5 }, }) ```

Prevention

  • Always set explicit timeouts on http.Client -- never use the zero-value client
  • Use per-request context.WithTimeout for granular deadline control
  • Implement circuit breakers for external service calls
  • Monitor request latency percentiles (p50, p95, p99) to set appropriate timeouts