Skip to content

gin-contrib/timeout

Repository files navigation

Timeout

Run Tests Trivy Security Scan codecov Go Report Card GoDoc

Timeout is a Gin middleware that wraps a handler and aborts its execution if a specified timeout is reached. This is useful for preventing slow handlers from blocking your server.


Table of Contents


Features

  • Abort request processing if it exceeds a configurable timeout.
  • Customizable timeout response.
  • Can be used as route or global middleware.
  • Compatible with other Gin middleware.
  • Buffered response writer to prevent partial responses.
  • Panic recovery within timeout handlers.

Installation

go get github.com/gin-contrib/timeout

Quick Start

A minimal example that times out a slow handler:

// _example/example01/main.go
package main

import (
  "log"
  "net/http"
  "time"

  "github.com/gin-contrib/timeout"
  "github.com/gin-gonic/gin"
)

func emptySuccessResponse(c *gin.Context) {
  time.Sleep(200 * time.Microsecond)
  c.String(http.StatusOK, "")
}

func main() {
  r := gin.New()

  r.GET("/", timeout.New(
    timeout.WithTimeout(100*time.Microsecond),
  ),
    emptySuccessResponse,
  )

  // Listen and Server in 0.0.0.0:8080
  if err := r.Run(":8080"); err != nil {
    log.Fatal(err)
  }
}

In this example, the handler will timeout because it sleeps for 200 microseconds while the timeout is set to 100 microseconds.


How It Works

The timeout middleware operates by:

  1. Buffering responses: It wraps the response writer with a buffered writer to prevent partial responses from being sent to the client.

  2. Running handlers in goroutines: Your handler executes in a separate goroutine with a context that can be cancelled.

  3. Race against time: The middleware waits for either:

    • Handler completion (writes buffered response to client)
    • Timeout expiry (writes timeout response instead)
    • Panic in handler (properly recovers and reports)
  4. Important limitation: Once response headers are written to the client, the timeout response cannot be sent. The middleware can only prevent responses if it catches the timeout before headers are flushed.

Default timeout: If not specified, the default timeout is 5 seconds.


API Reference

Configuration Options

timeout.New(opts ...Option) gin.HandlerFunc

Creates a new timeout middleware with the specified options.

Available Options

Option Description Default
WithTimeout(duration time.Duration) Sets the timeout duration 5 * time.Second
WithResponse(handler gin.HandlerFunc) Sets a custom timeout response handler Returns HTTP 408 with "Request Timeout" text

Example

timeout.New(
  timeout.WithTimeout(3 * time.Second),
  timeout.WithResponse(func(c *gin.Context) {
    c.JSON(http.StatusRequestTimeout, gin.H{
      "error": "Request took too long",
      "code": "TIMEOUT",
    })
  }),
)

Advanced Usage

1. Custom Timeout Response

You can define a custom response when a timeout occurs:

// Custom timeout response for a single route
func testResponse(c *gin.Context) {
  c.String(http.StatusRequestTimeout, "custom timeout response")
}

r.GET("/", timeout.New(
  timeout.WithTimeout(100*time.Microsecond),
  timeout.WithResponse(testResponse),
), func(c *gin.Context) {
  time.Sleep(200 * time.Microsecond)
  c.String(http.StatusOK, "")
})

2. Global Middleware

Apply the timeout middleware to all routes:

func testResponse(c *gin.Context) {
  c.String(http.StatusRequestTimeout, "timeout")
}

func timeoutMiddleware() gin.HandlerFunc {
  return timeout.New(
    timeout.WithTimeout(500*time.Millisecond),
    timeout.WithResponse(testResponse),
  )
}

func main() {
  r := gin.New()
  r.Use(timeoutMiddleware())
  r.GET("/slow", func(c *gin.Context) {
    time.Sleep(800 * time.Millisecond)
    c.Status(http.StatusOK)
  })
  if err := r.Run(":8080"); err != nil {
    log.Fatal(err)
  }
}

3. Logging Timeout Events

You can combine the timeout middleware with custom logging for timeout events:

import (
  "log/slog"
  "net/http"
  "time"

  "github.com/gin-contrib/timeout"
  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()

  r.Use(timeout.New(
    timeout.WithTimeout(100*time.Microsecond),
  ), func(c *gin.Context) {
    c.Next()
    if c.Writer.Status() == http.StatusRequestTimeout {
      slog.Error("request timeout")
    }
  })

  r.GET("/long", func(c *gin.Context) {
    time.Sleep(10 * time.Second)
    c.String(http.StatusOK, "long time ago")
  })

  s := &http.Server{
    Addr:              ":8000",
    Handler:           r,
    ReadTimeout:       30 * time.Second,
    WriteTimeout:      30 * time.Second,
    ReadHeaderTimeout: time.Second * 5,
  }

  if err := s.ListenAndServe(); err != nil {
    slog.Error("ListenAndServe failed", "err", err)
  }
}

4. Combining with Other Middleware

You can stack the timeout middleware with other middleware, such as authentication or logging:

func testResponse(c *gin.Context) {
  c.String(http.StatusRequestTimeout, "timeout")
}

// Custom timeout middleware
func timeoutMiddleware() gin.HandlerFunc {
  return timeout.New(
    timeout.WithTimeout(500*time.Millisecond),
    timeout.WithResponse(testResponse),
  )
}

// Example auth middleware
func authMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    debug := c.Query("debug")
    if debug != "true" {
      c.Next()
      return
    }
    c.AbortWithStatus(401)
  }
}

func main() {
  r := gin.New()
  r.Use(gin.Logger())
  r.Use(timeoutMiddleware())
  r.Use(authMiddleware())
  r.Use(gin.Recovery())

  r.GET("/", func(c *gin.Context) {
    time.Sleep(1 * time.Second)
    c.String(http.StatusOK, "Hello world!")
  })

  if err := r.Run(":8080"); err != nil {
    log.Fatal(err)
  }
}

Real-World Example: Testing Timeout

Suppose your handler always takes longer than the timeout:

// _example/example04/main.go (handler always times out)
r.GET("/", func(c *gin.Context) {
  time.Sleep(1 * time.Second)
  c.String(http.StatusOK, "Hello world!")
})

With a 500ms timeout, any request will return HTTP 408:

curl -i http://localhost:8080/

Expected response:

HTTP/1.1 408 Request Timeout
Content-Type: text/plain; charset=utf-8

timeout

More Examples

The _example directory contains additional usage scenarios:

Example Description Use Case
example01 Minimal route-level timeout Quick start - applying timeout to a single route
example02 Global middleware with custom response Production setup - protecting all endpoints with consistent timeout handling
example03 Logging timeout events + load testing Monitoring - tracking timeout occurrences with structured logging
example04 Integration with auth middleware Complex middleware chains - see the detailed README

Explore these examples for practical patterns and advanced integration tips.


Troubleshooting

Why is my handler still running after timeout?

Answer: The timeout middleware can only prevent the response from being sent to the client. It cannot forcefully terminate the goroutine running your handler. However, the client will receive a timeout response and the connection will be closed.

Best practice: Check c.Request.Context().Done() in long-running handlers to gracefully exit:

r.GET("/long", timeout.New(
  timeout.WithTimeout(2*time.Second),
), func(c *gin.Context) {
  for i := 0; i < 10; i++ {
    select {
    case <-c.Request.Context().Done():
      // Context cancelled, stop processing
      return
    default:
      // Do some work
      time.Sleep(500 * time.Millisecond)
    }
  }
  c.String(http.StatusOK, "done")
})

Why am I getting partial responses?

Answer: If your handler writes response headers before the timeout occurs, those headers cannot be recalled. The middleware uses a buffered writer to prevent this, but streaming responses or explicit header flushes can bypass the buffer.

Solution: Avoid calling c.Writer.Flush() or using streaming responses with timeout middleware.

What timeout value should I use?

Guidelines:

  • API endpoints: 5-30 seconds (default is 5s)
  • Database queries: 3-10 seconds
  • External API calls: 10-30 seconds
  • Long-running jobs: Consider using a job queue instead of HTTP with timeout

Tip: Set your timeout slightly lower than your load balancer or reverse proxy timeout to ensure your application responds first.

Can I use this with streaming responses?

Not recommended: The middleware buffers responses to prevent partial writes. Streaming responses (SSE, chunked encoding) are incompatible with this approach.

Alternative: For streaming endpoints, implement timeout logic within your handler using context.WithTimeout().

Does this work with panic recovery?

Yes: The middleware includes panic recovery. In debug mode (gin.SetMode(gin.DebugMode)), it will return detailed panic information. In release mode, it re-throws the panic to be handled by upstream middleware like gin.Recovery().


Contributing

Contributions are welcome! Please open an issue or submit a pull request.

License

This project is licensed under the MIT License - see the LICENSE file for details.