Using Go’s Context for Request‑Scoped Cancellation and Timeout in HTTP Servers
Ensure Go HTTP handlers honor client cancellations and server timeouts by propagating context, handling ctx.Done, and returning 499/504 status codes. Follow this step‑by‑step guide with code examples and verification checks.
05 Aug 2025, 16:32 UTC

Desired Outcome
Guarantee that every HTTP handler in a Go server respects client cancellations and server‑defined timeouts. The goal is to prevent goroutine leaks, avoid unnecessary work, and return the appropriate HTTP status codes (e.g., 499 for client cancel, 504 for timeout).
Prerequisites
- Go 1.13+ (context package stable)
- Basic
net/httpserver setup - Understanding of
context.Contextand its propagation rules - Access to a terminal with
curlor similar HTTP client
Focused Procedure
- Import the package
import ( "context" "net/http" ) - Change the handler signature
Standard
http.HandlerFuncreceiveshttp.ResponseWriterand*http.Request. Extract the context from the request and pass it explicitly to the handler logic.func main() { http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) { // Extract context once and use it throughout. ctx := r.Context() if err := doWork(ctx, w); err != nil { handleErr(w, err) } }) http.ListenAndServe(":8080", nil) } - Propagate the context to downstream operations
When calling database drivers, HTTP clients, or any long‑running goroutine, forward
ctxinstead ofcontext.Background()or a new context. Example with a simulated delay:func doWork(ctx context.Context, w http.ResponseWriter) error { // Simulate a long task that should be cancelable. select { case <-time.After(5 * time.Second): // Task completed. _, _ = w.Write([]byte("done")) return nil case <-ctx.Done(): // Context was cancelled or timed out. return ctx.Err() } } - Wrap the server with a timeout handler (optional)
If you want the server to enforce a hard timeout regardless of handler code, use
http.TimeoutHandleror set theReadTimeout/WriteTimeouton thehttp.Serverstruct.srv := &http.Server{ Addr: ":8080", Handler: http.TimeoutHandler(http.DefaultServeMux, 2*time.Second, "timeout"), ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 120 * time.Second, } if err := srv.ListenAndServe(); err != nil { log.Fatal(err) } - Handle context errors in the response
Map
context.Canceledto499andcontext.DeadlineExceededto504. This keeps the client informed and lets upstream services know the outcome.func handleErr(w http.ResponseWriter, err error) { switch err { case context.Canceled: w.WriteHeader(499) // Client closed the connection. _, _ = w.Write([]byte("request cancelled")) case context.DeadlineExceeded: w.WriteHeader(504) // Server timed out. _, _ = w.Write([]byte("request timed out")) default: w.WriteHeader(500) _, _ = w.Write([]byte("internal error")) } }
Expected Checks
- Run
curl --max-time 1 http://localhost:8080/workand observe a499response and acontext.Canceledlog entry. - Set the server timeout to
200msand send a request that sleeps for500ms; the client should receive a504and the handler should exit early withcontext.DeadlineExceeded. - Use
go test -raceon any concurrent code insidedoWorkto ensure no data races are introduced by usingctx. - Run
go vet ./...and confirm no unused context variables or global context leaks.
Recovery Options
- If
ctx.Err()is notnil, log the error, release any held resources (database connections, file handles, etc.), and terminate the goroutine chain. - When using third‑party libraries that do not accept a context, wrap them in a goroutine and use
selectonctx.Done()to cancel the operation. Example:select { case result := lib.DoWork(); case <-ctx.Done(): return ctx.Err() } - For long‑running pipelines, consider using
context.WithCancelat the top level of the handler and propagate the returned cancel function to all downstream stages. Call the cancel function in adeferblock to ensure cleanup even if the handler panics.
Limitations & Practical Checks
- Do not store a request’s context in a global variable; it will bleed across requests and cause subtle bugs.
- Never use
context.Background()for request‑scoped work; it ignores cancellations and can lead to orphaned goroutines. - Be aware that
http.TimeoutHandlerwrites its own timeout response and may mask the original error. If you need custom error handling, implement the timeout logic manually inside the handler. - Check that your database driver supports context; many drivers expose
QueryContextorExecContextmethods. - After deploying, monitor your application’s goroutine count with
pproforgo tool pprofto detect leaks when cancellations occur.
Concrete Example: Simulated Long‑Running Task
Below is a minimal server that demonstrates the full flow. The handler sleeps for 10 seconds unless the client cancels or the server times out at 3 seconds.
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/slow", slowHandler)
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Println("Listening on :8080")
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := doWork(ctx, w); err != nil {
handleErr(w, err)
}
}
func doWork(ctx context.Context, w http.ResponseWriter) error {
// Simulate a 10s operation.
select {
case <-time.After(10 * time.Second):
_, _ = w.Write([]byte("completed"))
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func handleErr(w http.ResponseWriter, err error) {
switch err {
case context.Canceled:
w.WriteHeader(499)
_, _ = w.Write([]byte("client cancelled"))
case context.DeadlineExceeded:
w.WriteHeader(504)
_, _ = w.Write([]byte("server timeout"))
default:
w.WriteHeader(500)
_, _ = w.Write([]byte("internal error"))
}
}
Run the server, then in a separate terminal:
curl -v http://localhost:8080/slow
# Wait 5 seconds, then cancel:
# (Ctrl+C or curl --max-time 5)
You should see a 499 response and the log entry client cancelled. If you set the server’s ReadTimeout to 3s, the client will receive a 504 after 3 seconds.
Conclusion
By explicitly extracting the request context, propagating it, and handling ctx.Done() correctly, you can build robust HTTP servers that respect client intent and avoid leaking resources. The pattern scales to microservices, database access, and any concurrent work that benefits from cancellation and deadlines.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.