fasthttp #

fasthttp is a low-level HTTP implementation for Go designed for high performance. Unlike the standard net/http, fasthttp uses aggressive object pooling, zero-allocation parsing, and buffer reuse to reduce GC pressure. As a result, fasthttp can serve 100K+ requests per second on consumer-grade hardware, far beyond the standard net/http. Frameworks like Fiber and Gear are built on top of fasthttp.

But working directly with fasthttp is different from working with a framework. You don’t get a router, middleware composition, or context helpers — you write or choose all of that yourself. This article covers a Docker Compose setup for fasthttp local development, from Dockerfiles, hot reload, to custom routing patterns and best practices.

Prerequisites #

Make sure you have installed:

  • Docker and Docker Compose (latest versions)
  • Go 1.22+ (optional, for host-side development)
  • Git

A fasthttp project structure:

my-fasthttp-app/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── router/
│   │   └── router.go
│   ├── handler/
│   │   ├── user.go
│   │   └── health.go
│   ├── middleware/
│   │   ├── logger.go
│   │   └── recover.go
│   ├── service/
│   │   └── user.go
│   └── model/
│       └── user.go
├── go.mod
├── go.sum
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .air.toml
├── .env
└── .dockerignore

What Makes fasthttp Different #

Before getting technical, understand fasthttp’s position in the Go ecosystem.

Aspectnet/httpfasthttp
Throughput~50K req/s100K+ req/s
Object poolingStandardAggressive (zero-allocation)
API styleStandardMethod-rich, non-standard
Middleware compatibility100%Not compatible without adapters
Handler typehttp.Handlerfasthttp.RequestHandler
Request/Responsehttp.Request, http.ResponseWriterfasthttp.RequestCtx
Body parsingr.Body.Read()ctx.PostBody()
Headersr.Header.Get()ctx.Request.Header.Peek()
Query stringsr.URL.Query().Get()ctx.QueryArgs().Peek()

High performance comes at a price: a non-standard API, and many Go ecosystem middleware pieces (OpenTelemetry, Prometheus, etc.) aren’t directly compatible. For most applications, net/http (or Gin/Chi) is enough. Use fasthttp only when you need extreme throughput.

Multi-Stage Dockerfile #

# syntax=docker/dockerfile:1.6
FROM golang:1.22-alpine AS builder

WORKDIR /app

RUN apk add --no-cache git ca-certificates

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server

FROM alpine:3.19

RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app

RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /out/server /app/server

EXPOSE 8080

ENTRYPOINT ["/app/server"]

A Development Dockerfile #

# Dockerfile.dev
FROM golang:1.22-alpine

WORKDIR /app

RUN apk add --no-cache git curl bash

RUN go install github.com/air-verse/air@latest

COPY go.mod go.sum ./
RUN go mod download

EXPOSE 8080

CMD ["air", "-c", ".air.toml"]

The Air configuration (.air.toml):

root = "."
tmp_dir = "tmp"

[build]
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ./cmd/server"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  include_dir = ["cmd", "internal"]
  include_ext = ["go", "yaml", "yml"]
  exclude_regex = ["_test.go"]
  exclude_unchanged = true

[log]
  time = true

docker-compose.yml #

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: fasthttp-app:dev
    container_name: fasthttp-api
    command: air -c .air.toml
    ports:
      - "8080:8080"
    volumes:
      - ./cmd:/app/cmd
      - ./internal:/app/internal
      - ./go.mod:/app/go.mod
      - ./go.sum:/app/go.sum
      - go-build:/app/tmp
    environment:
      - APP_ENV=development
      - SERVER_PORT=8080
      - DATABASE_URL=postgres://app:pass@db:5432/fasthttpapp?sslmode=disable
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    container_name: fasthttp-db
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=fasthttpapp
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d fasthttpapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: fasthttp-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

volumes:
  db-data:
  cache-data:
  go-build:

An Example fasthttp Application #

A Simple Router (internal/router/router.go):

fasthttp has no built-in router, so we write our own pattern matching. For complex routing, add the github.com/fasthttp/router or github.com/buaazp/fasthttprouter library.

package router

import (
    "github.com/buaazp/fasthttprouter"
    "myapp/internal/handler"
    "myapp/internal/middleware"
)

func New(userHandler *handler.UserHandler, healthHandler *handler.HealthHandler) *fasthttprouter.Router {
    r := fasthttprouter.New()

    // Global middleware
    r.GlobalOPTIONS = middleware.CORSHandler

    // Health
    r.GET("/health", healthHandler.Liveness)
    r.GET("/healthz", healthHandler.Healthz)
    r.GET("/readyz", healthHandler.Readyz)

    // API v1
    r.POST("/api/v1/users", middleware.JSONContentType(userHandler.Create))
    r.GET("/api/v1/users/:id", middleware.JSONContentType(userHandler.Get))
    r.GET("/api/v1/users", middleware.JSONContentType(userHandler.List))

    return r
}

Handler (internal/handler/user.go):

package handler

import (
    "encoding/json"
    "errors"
    "strconv"

    "github.com/buaazp/fasthttprouter"
    "github.com/valyala/fasthttp"

    "myapp/internal/service"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

type createUserRequest struct {
    Email    string `json:"email"`
    Name     string `json:"name"`
    Password string `json:"password"`
}

type errorResponse struct {
    Error string `json:"error"`
}

// Create: POST /api/v1/users
func (h *UserHandler) Create(ctx *fasthttp.RequestCtx) {
    var req createUserRequest
    if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
        ctx.SetStatusCode(fasthttp.StatusBadRequest)
        json.NewEncoder(ctx).Encode(errorResponse{Error: "invalid body"})
        return
    }
    if req.Email == "" || req.Name == "" || len(req.Password) < 8 {
        ctx.SetStatusCode(fasthttp.StatusBadRequest)
        json.NewEncoder(ctx).Encode(errorResponse{Error: "validation failed"})
        return
    }
    u, err := h.svc.Register(&ctx, req.Email, req.Name, req.Password)
    if err != nil {
        ctx.SetStatusCode(fasthttp.StatusBadRequest)
        json.NewEncoder(ctx).Encode(errorResponse{Error: err.Error()})
        return
    }
    ctx.SetStatusCode(fasthttp.StatusCreated)
    json.NewEncoder(ctx).Encode(u)
}

// Get: GET /api/v1/users/:id
func (h *UserHandler) Get(ctx *fasthttp.RequestCtx) {
    idStr := ctx.UserValue("id").(string)
    id, err := strconv.ParseUint(idStr, 10, 64)
    if err != nil {
        ctx.SetStatusCode(fasthttp.StatusBadRequest)
        json.NewEncoder(ctx).Encode(errorResponse{Error: "invalid id"})
        return
    }
    u, err := h.svc.Get(&ctx, id)
    if err != nil {
        ctx.SetStatusCode(fasthttp.StatusInternalServerError)
        json.NewEncoder(ctx).Encode(errorResponse{Error: err.Error()})
        return
    }
    if u == nil {
        ctx.SetStatusCode(fasthttp.StatusNotFound)
        json.NewEncoder(ctx).Encode(errorResponse{Error: "user not found"})
        return
    }
    json.NewEncoder(ctx).Encode(u)
}

// List: GET /api/v1/users?limit=10&offset=0
func (h *UserHandler) List(ctx *fasthttp.RequestCtx) {
    limit := 20
    offset := 0
    if v := ctx.QueryArgs().Peek("limit"); len(v) > 0 {
        if n, err := strconv.Atoi(string(v)); err == nil {
            limit = n
        }
    }
    if v := ctx.QueryArgs().Peek("offset"); len(v) > 0 {
        if n, err := strconv.Atoi(string(v)); err == nil {
            offset = n
        }
    }
    users, err := h.svc.List(&ctx, limit, offset)
    if err != nil {
        ctx.SetStatusCode(fasthttp.StatusInternalServerError)
        json.NewEncoder(ctx).Encode(errorResponse{Error: err.Error()})
        return
    }
    response := map[string]interface{}{
        "data":   users,
        "limit":  limit,
        "offset": offset,
    }
    json.NewEncoder(ctx).Encode(response)
}

Notice the main differences from net/http:

  • ctx.PostBody() for the body, not r.Body.Read()
  • ctx.UserValue("id") for path parameters, not chi.URLParam()
  • ctx.QueryArgs().Peek("key") for query strings, not r.URL.Query().Get("key")
  • ctx.SetStatusCode() for status codes, not w.WriteHeader()
  • json.NewEncoder(ctx).Encode(...) writes directly to the RequestCtx

Middleware #

Middleware in fasthttp follows the func(fasthttp.RequestHandler) fasthttp.RequestHandler pattern. This differs slightly from net/http’s func(http.Handler) http.Handler.

Logger Middleware (internal/middleware/logger.go):

package middleware

import (
    "log"
    "time"

    "github.com/valyala/fasthttp"
)

// Logger: log every request with method, path, status, and latency
func Logger(h fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        start := time.Now()
        h(ctx)
        log.Printf(
            "%s %s -> %d (%v)",
            ctx.Method(),
            ctx.Path(),
            ctx.Response.StatusCode(),
            time.Since(start),
        )
    }
}

Recover Middleware (internal/middleware/recover.go):

package middleware

import (
    "fmt"
    "runtime/debug"

    "github.com/valyala/fasthttp"
)

func Recover(h fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("panic: %v\n%s", r, debug.Stack())
                ctx.SetStatusCode(fasthttp.StatusInternalServerError)
                ctx.SetBodyString(`{"error":"internal server error"}`)
            }
        }()
        h(ctx)
    }
}

// JSONContentType: set Content-Type to application/json
func JSONContentType(h fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        ctx.Response.Header.SetContentType("application/json")
        h(ctx)
    }
}

// CORSHandler: handle preflight and set CORS headers
func CORSHandler(ctx *fasthttp.RequestCtx) {
    ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
    ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
    ctx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
    if ctx.Method() == "OPTIONS" {
        ctx.SetStatusCode(fasthttp.StatusNoContent)
        return
    }
}

The Main Server #

cmd/server/main.go:

package main

import (
    "context"
    "encoding/json"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/valyala/fasthttp"
    "github.com/redis/go-redis/v9"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "myapp/internal/handler"
    "myapp/internal/middleware"
    "myapp/internal/model"
    "myapp/internal/router"
    "myapp/internal/service"
)

func main() {
    // Database with retry
    var db *gorm.DB
    var err error
    dsn := os.Getenv("DATABASE_URL")
    for i := 0; i < 30; i++ {
        db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err == nil {
            break
        }
        log.Printf("waiting for db: %v", err)
        time.Sleep(2 * time.Second)
    }
    if err != nil {
        log.Fatal("db connection failed: ", err)
    }
    if err := db.AutoMigrate(&model.User{}); err != nil {
        log.Fatal("migrate failed: ", err)
    }

    // Redis
    rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_URL")})
    _ = rdb

    // Layers
    userRepo := newUserRepo(db)
    userSvc := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userSvc)
    healthHandler := handler.NewHealthHandler(db)

    // Router
    r := router.New(userHandler, healthHandler)

    // Apply global middleware (composed manually)
    handler := middleware.Recover(middleware.Logger(r.Handler))

    // Server
    server := &fasthttp.Server{
        Handler:            handler,
        Name:               "my-fasthttp-app",
        ReadTimeout:        10 * time.Second,
        WriteTimeout:       10 * time.Second,
        MaxConnsPerIP:      1000,
        MaxRequestsPerConn: 1000,
        Concurrency:        10000,
    }

    // Graceful shutdown
    go func() {
        log.Printf("listening on :8080")
        if err := server.ListenAndServe(":8080"); err != nil {
            log.Printf("server stopped: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := server.ShutdownWithContext(ctx); err != nil {
        log.Fatal(err)
    }
}

// Helper to inject dependencies (adjust to your repository structure)
func newUserRepo(db *gorm.DB) *service.UserRepo {
    return service.NewUserRepo(db)
}

// Health handler
type healthHandler struct {
    db *gorm.DB
}

func NewHealthHandler(db *gorm.DB) *healthHandler {
    return &healthHandler{db: db}
}

func (h *healthHandler) Liveness(ctx *fasthttp.RequestCtx) {
    ctx.SetStatusCode(200)
    json.NewEncoder(ctx).Encode(map[string]string{"status": "ok"})
}

func (h *healthHandler) Healthz(ctx *fasthttp.RequestCtx) {
    sqlDB, err := h.db.DB()
    if err != nil {
        ctx.SetStatusCode(503)
        json.NewEncoder(ctx).Encode(map[string]string{"error": "db unavailable"})
        return
    }
    if err := sqlDB.PingContext(ctx); err != nil {
        ctx.SetStatusCode(503)
        json.NewEncoder(ctx).Encode(map[string]string{"error": "db ping failed"})
        return
    }
    ctx.SetStatusCode(200)
    json.NewEncoder(ctx).Encode(map[string]string{"status": "ok", "database": "up"})
}

func (h *healthHandler) Readyz(ctx *fasthttp.RequestCtx) {
    ctx.SetStatusCode(200)
    json.NewEncoder(ctx).Encode(map[string]string{"status": "ready"})
}

Server Tuning #

fasthttp has many tuning options. The performance key lives here.

server := &fasthttp.Server{
    Handler:            handler,
    Name:               "myapp",
    ReadTimeout:        10 * time.Second,
    WriteTimeout:       10 * time.Second,
    IdleTimeout:        60 * time.Second,
    MaxConnsPerIP:      1000,    // max concurrent connections per IP
    MaxRequestsPerConn: 1000,    // close the connection after N requests
    Concurrency:        10000,   // max concurrent handlers
    ReadBufferSize:     4096,    // buffer size for reads
    WriteBufferSize:    4096,    // buffer size for writes
    DisableKeepalive:   false,   // keep-alive enabled
    KeepHijackedConns:  false,   // close hijacked conns
    TCPKeepalive:       true,    // TCP keep-alive
}

For high load, raise Concurrency and MaxConnsPerIP. For latency-sensitive APIs, lower ReadTimeout and WriteTimeout.

Writing your own router is repetitive work. Several libraries you can use:

LibraryProsCons
fasthttp/routerOfficial, simple, regex-basedNo typed parameters
buaazp/fasthttprouterLightweight, fastNot as feature-rich as go-chi
@fiber/router (from Fiber)Feature-richDepends on the Fiber ecosystem
Custom trieHighest performanceLots of your own code

For most cases, github.com/buaazp/fasthttprouter is enough. It supports :id parameters, * wildcards, and middleware.

Build and Run #

# Build
docker compose build

# Run
docker compose up -d

# View logs
docker compose logs -f api

# Benchmark (optional)
docker compose exec api wrk -t4 -c100 -d10s http://localhost:8080/health

# Stop
docker compose down

Access:

  • API: http://localhost:8080
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

# Register
curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Eko","password":"password123"}'

# Get a user
curl http://localhost:8080/api/v1/users/1

Benchmarking with wrk #

To measure throughput, use wrk inside the container or on the host.

# Install wrk in the container
docker compose exec api apk add wrk
docker compose exec api wrk -t4 -c100 -d10s http://localhost:8080/health

Output:

Running 10s test @ http://localhost:8080/health
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.13ms    1.45ms  25.46ms   78.91%
    Req/Sec    11.43k     1.32k   14.21k    78.95%
  456832 requests in 10.05s, 65.78MB read
Requests/sec:  45465.23
Transfer/sec:      6.55MB

45K req/s for a simple health endpoint. For endpoints that query the database, throughput drops significantly (depending on DB latency).

When fasthttp Fits #

Use fasthttp directly if:
  ✓ You need extreme throughput (>50K req/s)
  ✓ You have the expertise to handle a non-standard API
  ✓ High-throughput microservices (gateways, proxies)
  ✓ Benchmarks show net/http is the bottleneck

Use a framework on top of fasthttp (Fiber, Gear) if:
  ✓ You like Express-like APIs
  ✓ You need a middleware ecosystem
  ✓ Productivity matters more than low-level control

Use net/http / Gin / Chi if:
  ✓ Ordinary business applications
  ✓ You need ecosystem middleware compatibility
  ✓ A new Go team (idiomatic Go is easier to learn)
  ✓ 5-20K req/s throughput is enough

For 90% of web applications, net/http or Gin is more than enough. fasthttp excels in specific use cases: API gateways, real-time proxies, or services genuinely benchmarked for high throughput.

Best Practices #

Cache Dependency Modules #

Copy go.mod + go.sum first, then the source code. Super-fast incremental builds.

Use Air for Hot Reload #

Without hot reload, Go feels slow. Air watches files and rebuilds automatically.

Be Careful with Object Lifecycles #

fasthttp reuses RequestCtx. Don’t keep references to ctx after the handler returns. For data that must persist, copy it into a separate struct.

Tune the Server for Load #

Concurrency, MaxConnsPerIP, ReadTimeout, WriteTimeout — set them per workload. Defaults are fine for most cases, but for high throughput, raise Concurrency to 10K+.

Use Connection Pools for Databases #

GORM uses database/sql under the hood. Set MaxOpenConns, MaxIdleConns, and ConnMaxLifetime per load.

sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)

Healthchecks with DB Pings #

/healthz must check dependencies. Return 503 when the database is down. /readyz returns 200 when the app is ready.

Use a Mature Routing Library #

Don’t write your own router unless you need special features. fasthttp/router or buaazp/fasthttprouter is enough.

Bind-Mount Source Code in Dev #

Mount source code into the container. Air rebuilds automatically on every save.

Troubleshooting #

Memory Leaks #

fasthttp uses object pooling. If your handler stores references to RequestCtx or Response, they’ll be held in the pool and never GC’d. Always copy data that must persist.

Connection Refused #

Use depends_on: condition: service_healthy in Compose. Add a retry loop in the Go code.

Server Timeouts #

Raise ReadTimeout and WriteTimeout in fasthttp.Server. The 10s default is fine for normal APIs, but for large file uploads, raise to 60s+.

Routing Doesn’t Match #

fasthttprouter doesn’t support method override or implicit HEAD. Define explicit routes for the methods you need. Use r.GET, r.POST, etc.

CORS Preflight Fails #

Set r.GlobalOPTIONS = middleware.CORSHandler so preflight OPTIONS requests are handled globally.

Summary #

  • fasthttp is ideal for Go services needing extreme throughput (>50K req/s).
  • fasthttp uses a non-standard APIRequestCtx instead of http.Request/ResponseWriter. Not directly compatible with Go ecosystem middleware.
  • Multi-stage Dockerfiles for production: a builder stage with the Go toolchain, a slim runtime stage. The final image is < 20 MB.
  • Dockerfile.dev for development: full toolchain + Air for hot reload.
  • Hot reload with Air: watches .go files, rebuilds, restarts. Configured via .air.toml.
  • Go layer caching: copy go.mod + go.sum first, run go mod download, then copy the source.
  • Middleware follows the func(fasthttp.RequestHandler) fasthttp.RequestHandler pattern. Compose manually: Recover(Logger(handler)).
  • Routing libraries: github.com/buaazp/fasthttprouter for most cases. Use the official fasthttp/router for something simpler.
  • Server tuning: Concurrency, MaxConnsPerIP, ReadTimeout, WriteTimeout. Tune per workload.
  • Be careful with object lifecycles: RequestCtx is reused. Don’t keep references after the handler returns. Copy data that must persist.
  • Connection pools for databases: set MaxOpenConns, MaxIdleConns, ConnMaxLifetime.
  • Healthchecks check dependencies. Return 503 when the database is down.
  • Benchmark with wrk to verify throughput: wrk -t4 -c100 -d10s http://localhost:8080/health.
  • Use fasthttp if you need extreme throughput or high-performance microservices. Avoid it if a standard API is enough or you need the middleware ecosystem.
  • Best practices: cache dependencies, hot reload, careful object lifecycles, server tuning, connection pools, healthchecks.
  • Alternatives: Fiber for an Express-like API on fasthttp, Gin for net/http compatibility, Chi for a lightweight router.

← Previous: Flask   Next: Rails →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact