• English
  • Example: HTTP Client Set Timeouts

    HTTP requests can hang. Set a timeout.

    A timeout policy should define how long the entire operation may take and, when necessary, how long individual phases may wait.

    Quick Example

    package main
    
    import (
    	"fmt"
    	"net/http"
    	"time"
    )
    
    func main() {
    	client := &http.Client{
    		Timeout: 10 * time.Second,
    	}
    
    	resp, err := client.Get("https://example.com")
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	defer resp.Body.Close()
    
    	fmt.Println(resp.Status)
    }

    Client.Timeout is an overall wall-clock limit. It covers connection establishment, redirects, response headers, and reading the response body.

    For a simple request, this is often the right starting point.

    If you return early without consuming the body, close it. When connection reuse matters, a bounded drain may help the transport reuse the connection, but reuse is best-effort.

    Client.Timeout Is the Overall Deadline

    The default http.Client has a Timeout of zero, meaning it has no overall request timeout.

    That is dangerous when the remote server, network, or response body can stall indefinitely.

    client := &http.Client{
    	Timeout: 10 * time.Second,
    }

    The timeout covers the whole operation:

    connect
    
    TLS handshake
    
    send request
    
    wait for response headers
    
    read response body

    The important detail is that the timer does not stop when Do returns a Response.

    It remains active while the response body is being read.

    That means this can still time out:

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()
    
    _, err = io.ReadAll(resp.Body)

    A server that sends response headers quickly but then stops sending the body can still hit Client.Timeout.

    context.WithTimeout for One Operation

    Use a request context when the timeout belongs to the individual operation rather than the client itself.

    ctx, cancel := context.WithTimeout(
    	context.Background(),
    	5*time.Second,
    )
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	http.MethodGet,
    	"https://example.com",
    	nil,
    )
    if err != nil {
    	return err
    }
    
    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    A request context controls the lifetime of the outgoing request, including obtaining a connection, sending the request, waiting for response headers, and reading the response body.

    This makes it useful when the caller already has an operation deadline.

    For example, a request handled inside a larger operation should normally inherit that operation's context rather than inventing an unrelated timeout.

    Client.Timeout vs Request Context

    They solve related problems, but they express different ownership.

    MechanismScopeTypical use
    Client.TimeoutEvery request using the clientService-wide safety limit
    context.WithTimeoutOne request/operationCaller-specific deadline
    Dialer.TimeoutConnection establishmentBound TCP dialing
    TLSHandshakeTimeoutTLS handshakeBound HTTPS handshake
    ResponseHeaderTimeoutWaiting for response headersDetect slow upstream responses
    IdleConnTimeoutIdle pooled connectionsControl connection-pool lifetime

    A useful pattern is:

    caller deadline
    
    
    ┌─────────────────────────────────────────┐
    │           Client.Timeout                │
    │                                         │
    │  dial → TLS → request → headers → body │
    │   │      │               │              │
    │   └──────┴───────────────┴─ phase caps  │
    └─────────────────────────────────────────┘

    The shortest applicable deadline wins.

    Why One Timeout Is Sometimes Not Enough

    Client.Timeout prevents the entire request from running forever.

    But it does not tell you which phase is consuming the time budget.

    Consider:

    15s overall timeout
    
    connect ──────── 14s
    TLS                    ─ 100ms
    headers                    ─ 100ms
    body                         ─ 100ms

    The request technically has a timeout, but almost the entire budget was consumed establishing the connection.

    Phase-specific limits let you express stronger requirements:

    Dial              ≤ 3s
    TLS handshake     ≤ 3s
    Response headers  ≤ 5s
    Entire operation  ≤ 15s

    This gives you both:

    • an upper bound for the complete operation
    • sharper failure boundaries for individual phases

    The values are examples, not universal defaults. Choose them from your service's latency budget and upstream behavior.

    Connection Timeout

    A connection can stall before HTTP has even started.

    When using a custom Transport, configure the dialer:

    transport := http.DefaultTransport.(*http.Transport).Clone()
    
    transport.DialContext = (&net.Dialer{
    	Timeout: 3 * time.Second,
    }).DialContext

    Start with http.DefaultTransport.Clone() rather than constructing a new http.Transport from scratch.

    The default transport contains more than just its dial configuration, including connection-pool and protocol settings. Cloning it lets you change the timeout policy without accidentally replacing the rest of the transport configuration.

    The dial timeout applies to connection establishment.

    It does not limit:

    • TLS negotiation
    • waiting for response headers
    • reading the response body

    Those are separate phases.

    TLS Handshake Timeout

    For HTTPS, the TCP connection may succeed while the TLS handshake stalls.

    Configure a separate limit when you need one:

    transport.TLSHandshakeTimeout = 3 * time.Second

    This is particularly useful when diagnosing or controlling slow connection establishment to HTTPS services.

    Response Header Timeout

    A server may accept the request but take a long time before sending response headers.

    Use:

    transport.ResponseHeaderTimeout = 5 * time.Second

    This timeout starts after the request has been fully written.

    It does not include reading the response body.

    That distinction matters:

    request sent
    
    
    wait for headers ─────── ResponseHeaderTimeout
    
    
    read body ────────────── not covered by ResponseHeaderTimeout

    A slow body therefore still requires an overall deadline or another application-level policy.

    A Layered Client

    When an HTTP client needs both an overall deadline and phase-specific limits, combine them:

    package main
    
    import (
    	"net"
    	"net/http"
    	"time"
    )
    
    func newClient() *http.Client {
    	transport := http.DefaultTransport.(*http.Transport).Clone()
    
    	transport.DialContext = (&net.Dialer{
    		Timeout: 3 * time.Second,
    	}).DialContext
    
    	transport.TLSHandshakeTimeout = 3 * time.Second
    	transport.ResponseHeaderTimeout = 5 * time.Second
    
    	return &http.Client{
    		Transport: transport,
    		Timeout:   15 * time.Second,
    	}
    }

    The numbers above are illustrative.

    The important structure is:

    Dial                 3s
    TLS handshake        3s
    Response headers     5s
    Entire request      15s

    The overall timeout remains necessary because phase-specific timeouts do not cover every part of the request lifecycle.

    For example, ResponseHeaderTimeout does not limit response-body reading.

    Reuse the Client

    http.Client is safe for concurrent use.

    Create it once and reuse it:

    type API struct {
    	client *http.Client
    }
    
    func NewAPI() *API {
    	transport := http.DefaultTransport.(*http.Transport).Clone()
    
    	transport.ResponseHeaderTimeout = 5 * time.Second
    
    	return &API{
    		client: &http.Client{
    			Transport: transport,
    			Timeout:   15 * time.Second,
    		},
    	}
    }

    Do not create a new client and transport for every request.

    The connection pool belongs to the transport. Recreating transports prevents effective connection pooling and can create unnecessary connections and resource pressure.

    The usual production pattern is:

    service
    
       └── shared http.Client
    
                └── shared http.Transport
    
                           └── connection pool

    Timeout During Body Reading

    A timeout can happen after Do has already returned successfully.

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
    	return fmt.Errorf("read response body: %w", err)
    }

    The response may therefore be only partially read when the timeout occurs.

    Do not treat partially received data as a successful response unless the application protocol explicitly permits it.

    For a fixed-length or structured response, the operation should normally be considered failed if the body cannot be completely consumed and validated.

    Always close the response body.

    Timeout Errors

    There are two useful questions:

    1. Was the operation canceled or did its context deadline expire?
    2. Did a network operation report a timeout?

    For a request context:

    if errors.Is(err, context.DeadlineExceeded) {
    	// The request context reached its deadline.
    }

    For network-level timeout classification:

    var netErr net.Error
    if errors.As(err, &netErr) && netErr.Timeout() {
    	// A network operation timed out.
    }

    Preserve the original error when adding context:

    return fmt.Errorf("fetch upstream: %w", err)

    Do not flatten timeout errors into strings. Callers may need to distinguish cancellation, deadline expiration, timeout, and other network failures.

    Client.Timeout Is Not an Idle Timeout

    This is an important distinction for streaming responses.

    Client.Timeout is an overall deadline.

    It does not mean:

    "Fail if no bytes arrive for 5 seconds."

    Suppose a server sends one small chunk every few seconds:

    request
    
      ├── data
      ├────── wait
      ├── data
      ├────── wait
      ├── data
      └── ...

    An overall timeout eventually terminates the request, but it does not provide an idle-period policy.

    Likewise, ResponseHeaderTimeout stops being relevant once the headers have arrived.

    For legitimately long-lived streams, do not use Client.Timeout as an idle timeout.

    Streaming protocols may need their own application-level liveness or heartbeat rules.

    Request Uploads Also Consume the Overall Timeout

    The overall client timeout includes sending the request.

    This matters for large uploads.

    connect
    
    TLS
    
    upload request body ────────┐
       │                        │
       └────────────────────────┤ Client.Timeout
    
                         wait for headers
    
                             read body

    ResponseHeaderTimeout does not start until the request has been fully written.

    So a slow upload can consume most of the overall timeout before the response-header timer even becomes relevant.

    This is another reason to think in terms of an overall deadline plus phase-specific policies rather than one universal timer.

    Do Not Build a Timeout With a Timer Goroutine

    Avoid patterns such as:

    go func() {
    	time.Sleep(10 * time.Second)
    	// somehow "kill" the request
    }()

    A timer does not cancel an HTTP operation by itself.

    Use the cancellation mechanisms provided by net/http:

    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    req = req.WithContext(ctx)

    or:

    client := &http.Client{
    	Timeout: 10 * time.Second,
    }

    The context approach is usually preferable when the timeout belongs to a particular operation.

    Timeouts and Retries

    A timeout is not automatically a retryable failure.

    Before retrying, ask:

    • Was the operation safe to retry?
    • Did the server possibly receive the request?
    • Is there enough time left in the caller's deadline?

    For GET requests, retrying selected transient failures may be reasonable.

    For side-effecting requests such as POST, a timeout does not tell you whether the server processed the request before the client timed out.

    Do not turn:

    timeout
    
    retry
    
    timeout
    
    retry

    into an unbounded extension of the original operation.

    Retries should remain inside the original deadline.

    For APIs that support idempotency keys, use them when the operation semantics require safe retrying.

    Common Mistakes

    No Overall Timeout

    client := &http.Client{}

    The client has no overall timeout.

    For outbound requests to untrusted or unreliable systems, this is usually an unsafe default.

    Only Setting ResponseHeaderTimeout

    transport.ResponseHeaderTimeout = 5 * time.Second

    This does not protect you from a response body that stalls after the headers arrive.

    Only Setting a Dial Timeout

    net.Dialer{
    	Timeout: 3 * time.Second,
    }

    This protects connection establishment, not the rest of the request.

    Creating a New Transport for Every Request

    This defeats effective connection pooling and creates unnecessary connection-management overhead.

    Customize and reuse a transport instead.

    Treating Timeout as Success

    A partial response is not a successful response merely because some bytes arrived before the timeout.

    Validate the complete response according to the application protocol.

    Retrying Every Timeout

    A timeout does not prove that the server did not process the request.

    Be especially careful with non-idempotent operations.

    Rule of Thumb

    SituationUse
    Every request needs an upper boundhttp.Client.Timeout
    One operation has its own deadlinecontext.WithTimeout
    TCP connection may take too longnet.Dialer.Timeout
    HTTPS handshake may take too longTLSHandshakeTimeout
    Server takes too long to send headersResponseHeaderTimeout
    Pooled connections should not remain idle foreverIdleConnTimeout
    Long-lived response streamAvoid using Client.Timeout as an idle timeout

    A practical baseline for a normal API client is:

    request context
          +
    overall Client.Timeout
          +
    phase-specific Transport timeouts where useful
          +
    reused Client and Transport

    Do not add every timeout simply because it exists.

    Add a timeout when you can explain which failure boundary it protects.

    Key Takeaways

    • HTTP requests can hang. Set an overall timeout.
    • Client.Timeout covers the entire request lifecycle, including response-body reads.
    • Use context.WithTimeout when the deadline belongs to one operation or its caller.
    • Dialer.Timeout, TLSHandshakeTimeout, and ResponseHeaderTimeout protect specific phases.
    • ResponseHeaderTimeout does not cover response-body reading.
    • Client.Timeout is an overall deadline, not an idle timeout for streaming responses.
    • Customize http.DefaultTransport.Clone() instead of rebuilding a transport from an empty struct unless you intentionally want to define all transport settings yourself.
    • Reuse http.Client and http.Transport.
    • A timeout can leave a response body partially read; close it and treat incomplete data according to the application protocol.
    • Classify timeout errors with net.Error.Timeout() when appropriate, and preserve errors with %w.
    • Do not retry timeouts blindly, especially for side-effecting requests.
    • Choose timeout values from actual latency budgets and upstream behavior, not arbitrary numbers.