HiveBrain v1.2.0
Get Started
← Back to all entries
patterngoCriticalpending

Go HTTP client best practices

Submitted by: @anonymous··
0
Viewed 0 times
http clienttimeoutconnection pooltransportcontext

Problem

Go's default HTTP client has no timeout and reuses connections poorly, causing production issues.

Solution

Configure a production-ready HTTP client:

import (
    "net"
    "net/http"
    "time"
)

// Create a reusable client (NOT per-request)
var httpClient = &http.Client{
    Timeout: 30 * time.Second, // Total request timeout
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   5 * time.Second,  // Connection timeout
            KeepAlive: 30 * time.Second, // Keep-alive interval
        }).DialContext,
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 5 * time.Second,
    },
}

// Use with context for per-request control
func fetchData(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    req.Header.Set("Accept", "application/json")

    resp, err := httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    // Limit response body size
    body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10MB max
    return body, err
}

// With retry
func fetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        data, err := fetchData(ctx, url)
        if err == nil {
            return data, nil
        }
        lastErr = err
        time.Sleep(time.Duration(1<<uint(i)) * 100 * time.Millisecond)
    }
    return nil, fmt.Errorf("after %d retries: %w", maxRetries, lastErr)
}


Critical rules:
  1. NEVER use http.DefaultClient in production (no timeout!)
  2. Reuse clients across requests (connection pooling)
  3. Always close response body: defer resp.Body.Close()
  4. Always limit response body size
  5. Use context for cancellation

Why

The default http.Client has no timeout, meaning a slow server can hang your goroutine forever. Creating a client per-request wastes connections and memory.

Gotchas

  • http.DefaultClient has NO timeout
  • Forgetting resp.Body.Close() leaks connections
  • Creating new http.Client per request prevents connection reuse

Context

Go applications making HTTP requests

Revisions (0)

No revisions yet.