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.
| Aspect | net/http | fasthttp |
|---|---|---|
| Throughput | ~50K req/s | 100K+ req/s |
| Object pooling | Standard | Aggressive (zero-allocation) |
| API style | Standard | Method-rich, non-standard |
| Middleware compatibility | 100% | Not compatible without adapters |
Handler type | http.Handler | fasthttp.RequestHandler |
| Request/Response | http.Request, http.ResponseWriter | fasthttp.RequestCtx |
| Body parsing | r.Body.Read() | ctx.PostBody() |
| Headers | r.Header.Get() | ctx.Request.Header.Peek() |
| Query strings | r.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, notr.Body.Read()ctx.UserValue("id")for path parameters, notchi.URLParam()ctx.QueryArgs().Peek("key")for query strings, notr.URL.Query().Get("key")ctx.SetStatusCode()for status codes, notw.WriteHeader()json.NewEncoder(ctx).Encode(...)writes directly to theRequestCtx
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.
Popular Routing Libraries #
Writing your own router is repetitive work. Several libraries you can use:
| Library | Pros | Cons |
|---|---|---|
fasthttp/router | Official, simple, regex-based | No typed parameters |
buaazp/fasthttprouter | Lightweight, fast | Not as feature-rich as go-chi |
@fiber/router (from Fiber) | Feature-rich | Depends on the Fiber ecosystem |
| Custom trie | Highest performance | Lots 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 API —
RequestCtxinstead ofhttp.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
.gofiles, rebuilds, restarts. Configured via.air.toml.- Go layer caching: copy
go.mod+go.sumfirst, rungo mod download, then copy the source.- Middleware follows the
func(fasthttp.RequestHandler) fasthttp.RequestHandlerpattern. Compose manually:Recover(Logger(handler)).- Routing libraries:
github.com/buaazp/fasthttprouterfor most cases. Use the officialfasthttp/routerfor something simpler.- Server tuning:
Concurrency,MaxConnsPerIP,ReadTimeout,WriteTimeout. Tune per workload.- Be careful with object lifecycles:
RequestCtxis 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.