SkillAgentSearch skills...

Timeout

Timeout middleware for Gin

Install / Use

/learn @gin-contrib/Timeout
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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 ti

Related Skills

View on GitHub
GitHub Stars231
CategoryDevelopment
Updated15h ago
Forks42

Languages

Go

Security Score

95/100

Audited on Mar 28, 2026

No findings