go 84 lines · 3 tabs

Propagating a Context Deadline for Per-Request Timeouts in Go HTTP Handlers

Shared by codesnips Jul 2026
3 tabs
package store

import (
	"context"
	"database/sql"
)

type Order struct {
	ID     string
	Status string
	Total  int64
}

type OrderStore struct {
	db *sql.DB
}

func NewOrderStore(db *sql.DB) *OrderStore {
	return &OrderStore{db: db}
}

func (s *OrderStore) GetByID(ctx context.Context, id string) (*Order, error) {
	const q = `SELECT id, status, total_cents FROM orders WHERE id = $1`

	var o Order
	err := s.db.QueryRowContext(ctx, q, id).Scan(&o.ID, &o.Status, &o.Total)
	if err != nil {
		return nil, err
	}
	return &o, nil
}
3 files · go Explain with highlit

This snippet shows how a per-request timeout is enforced end to end in a Go service by attaching a deadline to the request's context.Context and letting every downstream call observe it. The key idea is that Go does not interrupt running goroutines; instead, cooperating APIs check the context and abort when it is cancelled. By deriving a child context with context.WithTimeout at the edge and threading it through the store and the database driver, a slow query no longer holds a connection open indefinitely — it is cancelled precisely when the request budget expires.

In timeout middleware, TimeoutMiddleware wraps a handler and replaces the incoming request context with one produced by context.WithTimeout. The defer cancel() is essential: it releases the timer and any resources associated with the context once the handler returns, even on the fast path, avoiding a goroutine and timer leak. The middleware is deliberately thin — it does not itself write a timeout response, because the deadline may fire deep inside a handler where the writer is better positioned to react.

OrderStore demonstrates propagation into the data layer. Every method takes ctx as its first argument and passes it to QueryRowContext, so the database/sql driver can send a cancellation to Postgres when the deadline passes. This is the payoff of the pattern: the timeout is not advisory, it actually tears down in-flight work rather than letting it complete unnoticed.

orders handler ties it together. GetOrder calls the store with r.Context(), which already carries the deadline installed by the middleware. When a lookup exceeds the budget, the error is inspected with errors.Is(err, context.DeadlineExceeded) and translated into a 504 Gateway Timeout; a genuine missing row maps to 404. Distinguishing these cases matters, because a deadline error is a server-side capacity signal, not a client error.

The trade-off is discipline: the pattern only works if every blocking call accepts and honors a context, so a single ctx-less call becomes a silent hole where the timeout is ignored. It also means the deadline is shared across all downstream operations in a request, so a slow first query can starve later ones. Reaching for this approach makes sense whenever a service must bound tail latency and protect scarce resources like connection pool slots.


Related snips

Share this code

Here's the card — post it anywhere.

Propagating a Context Deadline for Per-Request Timeouts in Go HTTP Handlers — share card
Link copied