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.
- Timeout
- 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.
go get github.com/gin-contrib/timeoutA 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.
The timeout middleware operates by:
-
Buffering responses: It wraps the response writer with a buffered writer to prevent partial responses from being sent to the client.
-
Running handlers in goroutines: Your handler executes in a separate goroutine with a context that can be cancelled.
-
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)
-
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.
Creates a new timeout middleware with the specified 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 |
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",
})
}),
)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, "")
})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)
}
}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)
}
}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)
}
}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
timeoutThe _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.
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")
})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.
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.
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().
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().
Contributions are welcome! Please open an issue or submit a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.