Timeouts and Deadlines
WithTimeout is WithDeadline with the arithmetic done for you, and anything that leaves your process needs one.
ctx, cancel := context.WithTimeout(parent, 2*time.Second); defer cancel()A timeout is a cancellation that happens on its own after a set amount of time. It exists because a call that leaves your process has no natural end. A slow database, a wedged network connection or an unresponsive service will keep a goroutine waiting for as long as you let it, and every waiting goroutine holds a connection and some memory.
The two functions
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
// The same thing, written out:
ctx, cancel := context.WithDeadline(parent, time.Now().Add(2*time.Second))
defer cancel()
WithTimeout is documented as returning exactly WithDeadline(parent, time.Now().Add(timeout)). A timeout is a duration from now; a deadline is an absolute instant. Use WithTimeout for a single call's budget and WithDeadline when several steps must share one wall-clock cut-off.
What happens when it fires
ctx.Done() closes and ctx.Err() returns context.DeadlineExceeded rather than context.Canceled. Compare with errors.Is(err, context.DeadlineExceeded), because the error can arrive wrapped by the library that noticed it.
ctx.Deadline() returns the instant and true, or the zero time and false when there is no deadline.
What trips people up
A deadline is only ever tightened. WithDeadline adjusts the deadline to be no later than the one you ask for, so a child asking for ten seconds inside a parent with one second left still stops after one second.
Go 1.21 added context.WithTimeoutCause and context.WithDeadlineCause, which take an extra error that context.Cause then reports.
Example
package main
import (
"context"
"errors"
"fmt"
"time"
)
// fetch waits for a reply, or for the context to run out of time.
func fetch(ctx context.Context, reply <-chan string) (string, error) {
select {
case r := <-reply:
return r, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Shorthand for:
// context.WithDeadline(ctx, time.Now().Add(50*time.Millisecond))
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
// Nobody ever sends on this channel, so the deadline decides the outcome.
_, err := fetch(ctx, make(chan string))
fmt.Println(err)
fmt.Println(errors.Is(err, context.DeadlineExceeded))
}
// Output:
// context deadline exceeded
// true
When to use it
- Stop a slow upstream API call before the caller's own deadline runs out.
- Bound a database query so one bad plan cannot pin a connection all day.
- Give a whole HTTP handler a single wall-clock budget shared by all its steps.
- Fail fast on a connection to a host that accepts packets but never replies.
More examples
A budget on an outbound HTTP call
http.NewRequestWithContext attaches the context to the request, and the client abandons the call when it is cancelled, including a read that has already started streaming a body. This is the default shape for any call to another service. Note that the timeout covers reading the body too, not just getting the response headers.
func fetchUser(ctx context.Context, id string) ([]byte, error) {
// Two seconds, or less if the caller's context runs out first.
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
url := "https://api.example.com/users/" + id
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}One deadline shared by several steps
When a caller is promised an answer within half a second, the whole handler needs one cut-off, not a fresh half second per step. An absolute deadline expresses that directly and every step below it inherits the remaining time. Check ctx.Deadline() if a step wants to decide whether it has time to bother starting.
func report(ctx context.Context, userID int64) (Report, error) {
// WithTimeout(ctx, 500*time.Millisecond) is the shorthand for this line.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(500*time.Millisecond))
defer cancel()
user, err := loadUser(ctx, userID)
if err != nil {
return Report{}, err
}
// The second call inherits the same instant, so it gets whatever
// time the first call left behind.
orders, err := loadOrders(ctx, userID)
if err != nil {
return Report{}, err
}
return Report{User: user, Orders: orders}, nil
}Naming which timeout fired (Go 1.21)
A request with four nested timeouts produces four identical context deadline exceeded errors, and the log cannot tell you which one expired. WithTimeoutCause labels each one. Use it in any code path with more than one timeout, so an alert points at a service rather than at the context package.
var errBillingSlow = errors.New("billing service too slow")
func charge(ctx context.Context, amount int64) error {
ctx, cancel := context.WithTimeoutCause(ctx, time.Second, errBillingSlow)
defer cancel()
if err := billing.Charge(ctx, amount); err != nil {
// err is context.DeadlineExceeded, which says nothing about where.
// context.Cause names the step that ran out of time.
log.Printf("charge failed: %v (cause: %v)", err, context.Cause(ctx))
return err
}
return nil
}
Discussion