Cancellation and timeouts with context in Go

1 min readThis post is also available in Turkish →

Tested with: Go 1.26

Contents
  1. Pass context to every call
  2. Let the caller set the timeout
  3. Listen on ctx.Done() inside goroutines

When an HTTP request is canceled, the database query and external service calls started for that request should stop too. In Go, the mechanism for this is context.Context.

Pass context to every call

The most common mistake I see is using context.Background() inside a handler:

handler.go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
// Wrong: the query keeps running even if the client closes the connection.
user, err := h.repo.Find(context.Background(), r.PathValue("id"))
// ...
}

The right way is to use the request’s own context:

handler.go
user, err := h.repo.Find(r.Context(), r.PathValue("id"))

Let the caller set the timeout

Every call to an external service should have an upper bound. Defining that bound where the call is made lets the reader understand the behavior at a glance.

ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
resp, err := h.payments.Charge(ctx, req)
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "payment service did not respond", http.StatusGatewayTimeout)
return
}

Skipping the defer cancel() line means resources are not released until the timeout expires.

Tip

The lostcancel check in go vet catches cancel functions that are never called. It’s worth running in CI.

Listen on ctx.Done() inside goroutines

A background goroutineGoroutine A lightweight concurrent unit of execution managed by the Go runtime. should exit when the context is canceled:

for {
select {
case <-ctx.Done():
return ctx.Err()
case msg := <-messages:
process(msg)
}
}

You can run the example below to see how the timeout works:

package main
import (
"context"
"fmt"
"time"
)
func slowQuery(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
err := slowQuery(ctx)
fmt.Println("result:", err) // result: context deadline exceeded
}

Run in Go Playground ↗

Using goleak in tests to catch leaks is a good habit:

GitHububer-go/goleakGoroutine leak detector★ 5.3K stars · Go
Revision history (1)
  1. Alt bilgiye build imzası, 404 ve çevrimdışı sayfalarına terminal görünümü ekleae75462

Keyboard shortcuts

Search and commands
K
Search
/
Home
gh
Posts
gy
Next post
j
Previous post
k
Toggle theme
t
Show this list
?