Go/Gin

Go Gin Context Timeouts and Cancellation Propagation to Databases

5 min read by DebuggedIt

Quick answer

In a Gin handler, pass c.Request.Context() to every database and outbound call, and wrap it with context.WithTimeout to enforce a deadline. When the client...

In a Gin handler, pass c.Request.Context() to every database and outbound call, and wrap it with context.WithTimeout to enforce a deadline. When the client disconnects or the deadline passes, the context is canceled and context-aware drivers stop the query. Using the context-aware methods, such as QueryContext, is what makes this work.

Why Does Context Propagation Matter in a Gin API?

Without cancellation, a slow query keeps running even after the client gave up. Under load, those abandoned queries hold connections, consume database CPU, and make the slowdown worse. A deadline turns an unbounded wait into a predictable failure that you can handle.

Gin exposes the standard request context through c.Request.Context(). The gin.Context itself also implements the context interface, but passing the request's context is the clearer and safer choice, especially when work might outlive the handler.

There is also a capacity angle. A connection pool has a fixed size, so every query that runs longer than necessary reduces the number of requests you can serve. Deadlines are a form of backpressure: they free pool connections quickly, which keeps one slow dependency from exhausting everything else.

How Do You Apply Timeouts to Database Calls?

Create a child context with a deadline for each unit of work and always call cancel to release resources. Pass that context to the database method.

func (h *Handler) GetOrder(c *gin.Context) {
    ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
    defer cancel()

    order, err := h.repo.FindOrder(ctx, c.Param("id"))
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            c.JSON(http.StatusGatewayTimeout, gin.H{"error": "timeout"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"})
        return
    }
    c.JSON(http.StatusOK, order)
}

Inside the repository, use the context variants of the database API, such as db.QueryRowContext(ctx, query, args...). The non-context variants ignore cancellation entirely, which is a common source of runaway queries.

Whether the server-side query is actually terminated depends on the driver and database. Most drivers send a cancel request, but verify the behavior for your driver in its documentation. You can also set a statement timeout on the database side as a second line of defense, so a query cannot run forever even if cancellation fails.

Where Should Timeouts Be Set?

Use layers. Each layer has a different job, and the deadlines should nest from the outside in, with inner timeouts shorter than outer ones.

  • Server level: ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout on http.Server protect against slow clients.
  • Request level: a deadline for the whole handler, set in middleware.
  • Call level: tighter limits for each database or HTTP dependency.
func Timeout(d time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), d)
        defer cancel()
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

Note that this middleware only sets the deadline. It does not write a response by itself, so handlers still need to check for errors and respond properly. Choosing the numbers matters too: base them on observed latency percentiles for each dependency rather than round guesses, and revisit them as traffic grows.

What Mistakes Break Cancellation?

  • Using context.Background() inside handlers, which discards the client's cancellation and deadline.
  • Starting a goroutine that uses the request context after the handler returns. The context is canceled when the request ends.
  • Forgetting defer cancel(), which leaks timers until the deadline passes.
  • Retrying after cancellation with the same context, which fails immediately.

For work that must continue after the response, such as sending an email, hand it to a queue or create a detached context deliberately, understanding that you are choosing to ignore client cancellation.

Retries deserve a budget of their own. If each of three retries gets a full two second timeout, a single request can take six seconds, which breaks the outer deadline. Derive retry timeouts from the time remaining on the parent context instead of resetting them.

Frequently Asked Questions

Should I use c or c.Request.Context() for database calls?

Prefer c.Request.Context(). It is the standard library context, works with any database package, and makes the dependency on the request lifecycle explicit.

What happens when the client disconnects?

The request context is canceled. Context-aware calls return an error, which you should handle without trying to write a normal response.

What status code should a timeout return?

504 Gateway Timeout or 503 Service Unavailable are common choices. Pick one consistently and document it for API clients.

Does a context timeout guarantee the query stops?

It signals cancellation. Actual termination depends on the driver and database, so check your driver documentation and test it.

Can I share one timeout for every endpoint?

You can start with a global default, but endpoints differ. Exports and reports need longer limits than simple lookups, so override the default for the slow ones.

Summary

Propagate the request context everywhere, set layered timeouts, use context-aware database methods, and handle DeadlineExceeded explicitly. Avoid detached contexts unless you intend them, and confirm cancellation behavior in your driver documentation.