Skip to main content
Version: Next

Timeout

The timeout middleware enforces a deadline on handler execution. It wraps handlers with context.WithTimeout, exposes the derived context through c.Context(), and returns 503 Service Unavailable when the deadline is exceeded.

How It Works​

When a timeout occurs, the middleware returns immediately without waiting for the handler to finish. This is achieved through Fiber's Abandon mechanism:

  1. The handler runs in a goroutine with a timeout context
  2. On timeout, the middleware marks the context as "abandoned" and returns 503 immediately
  3. The handler goroutine can continue safely (e.g., for cleanup) without blocking the response
  4. A background cleanup goroutine waits for the handler to finish and performs context cleanup

Handlers can detect the timeout by listening on c.Context().Done() and return early. This is the recommended pattern for cooperative cancellation.

If a handler panics, the middleware catches it and returns 500 Internal Server Error.

Why 503​

A handler that outlives its deadline is slow on the server's side: the request arrived complete. 408 Request Timeout means the server did not receive a complete request in time (RFC 9110 Β§15.5.9), and it tells the client that it may send the request again, while the timed-out handler can still be running and may finish its work. The default is therefore 503 Service Unavailable (Β§15.6.4). Use 504 Gateway Timeout (Β§15.6.5) when the handler stands in front of an upstream that did not answer in time, and return it from OnTimeout:

timeout.Config{
Timeout: 5 * time.Second,
OnTimeout: func(c fiber.Ctx) error {
return fiber.ErrGatewayTimeout
},
}

Known limitations​

  • The timed-out handler keeps running in its own goroutine while the middleware returns fiber.ErrServiceUnavailable (or runs OnTimeout). A handler must stop using the context once c.Context() is done: one that keeps writing the response races with OnTimeout.

  • The error is returned to the outer middleware, but the app's ErrorHandler is not run for a timed-out request: fasthttp already holds the response it will send, so the handler could not change it, and the timed-out handler may still be writing to the context.

  • Timed-out requests abandon their fiber.Ctx to avoid data races with the core request handler. A *fiber.DefaultCtx is reclaimed once both the timed-out handler has finished and Fiber has released the context, so it does return to the pool. A custom Ctx implementation has no such wiring: its context stays out of the pool, and each timed-out request leaks one. Calling ForceRelease yourself is only safe if you can guarantee that no goroutine (including Fiber internals) will touch the context anymore.

caution

timeout.New wraps your final handler and can't be added with app.Use or used in a middleware chain. Register it per route and avoid calling c.Next() inside the wrapped handlerβ€”doing so will panic.

Signatures​

func New(handler fiber.Handler, config ...timeout.Config) fiber.Handler

Examples​

Basic example​

The following program times out any request that takes longer than two seconds. The handler simulates work with sleepWithContext, which stops when the context is canceled:

package main

import (
"context"
"fmt"
"log"
"time"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/timeout"
)

func sleepWithContext(ctx context.Context, d time.Duration) error {
select {
case <-time.After(d):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func main() {
app := fiber.New()

handler := func(c fiber.Ctx) error {
delay, _ := time.ParseDuration(c.Params("delay") + "ms")
if err := sleepWithContext(c.Context(), delay); err != nil {
return fmt.Errorf("%w: execution error", err)
}
return c.SendString("finished")
}

app.Get("/sleep/:delay", timeout.New(handler, timeout.Config{
Timeout: 2 * time.Second,
}))

log.Fatal(app.Listen(":3000"))
}

Use these requests to see the middleware in action:

curl -i http://localhost:3000/sleep/1000 # finishes within the timeout
curl -i http://localhost:3000/sleep/3000 # returns 503 Service Unavailable

Config​

PropertyTypeDescriptionDefault
Nextfunc(fiber.Ctx) boolFunction to skip this middleware when it returns true.nil
Timeouttime.DurationTimeout duration for requests. 0 or a negative value disables the timeout.0
OnTimeoutfiber.HandlerHandler executed when a timeout occurs. It may write the response itself or return a *fiber.Error, whose status and message then form the response; otherwise the default 503 is sent. Defaults to returning fiber.ErrServiceUnavailable.nil
Errors[]errorCustom errors treated as timeout errors.nil

Use with a custom error​

var ErrFooTimeOut = errors.New("foo context canceled")

func main() {
app := fiber.New()
h := func(c fiber.Ctx) error {
sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms")
if err := sleepWithContextWithCustomError(c.Context(), sleepTime); err != nil {
return fmt.Errorf("%w: execution error", err)
}
return nil
}

app.Get("/foo/:sleepTime", timeout.New(h, timeout.Config{Timeout: 2 * time.Second, Errors: []error{ErrFooTimeOut}}))
log.Fatal(app.Listen(":3000"))
}

func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return ErrFooTimeOut
case <-timer.C:
}
return nil
}

Sample usage with a database call​

func main() {
app := fiber.New()
db, _ := gorm.Open(postgres.Open("postgres://localhost/foodb"), &gorm.Config{})

handler := func(ctx fiber.Ctx) error {
tran := db.WithContext(ctx.Context()).Begin()

if tran = tran.Exec("SELECT pg_sleep(50)"); tran.Error != nil {
return tran.Error
}

if tran = tran.Commit(); tran.Error != nil {
return tran.Error
}

return nil
}

app.Get("/foo", timeout.New(handler, timeout.Config{Timeout: 10 * time.Second}))
log.Fatal(app.Listen(":3000"))
}