fasthttp #
fasthttp adalah HTTP implementation low-level untuk Go yang dirancang untuk performa tinggi. Tidak seperti net/http standar, fasthttp menggunakan object pooling agresif, zero-allocation parsing, dan reuse buffer untuk mengurangi GC pressure. Hasilnya, fasthttp bisa melayani 100K+ request per detik pada hardware consumer-grade, jauh di atas net/http standar. Framework seperti Fiber dan Gear dibangun di atas fasthttp.
Tapi bekerja langsung dengan fasthttp itu berbeda dari framework. Kamu tidak punya router, middleware composition, atau context helper — semuanya harus kamu tulis atau pilih sendiri. Artikel ini membahas setup Docker Compose untuk local development fasthttp, dari Dockerfile, hot reload, hingga pattern routing kustom dan best practice.
Prasyarat #
Pastikan sudah terinstall:
- Docker dan Docker Compose versi terbaru
- Go 1.22+ (opsional, untuk development di host)
- Git
Struktur project fasthttp:
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
Apa yang Membuat fasthttp Berbeda #
Sebelum masuk ke teknis, pahami dulu posisi fasthttp di ekosistem Go.
| Aspek | net/http |
fasthttp |
|---|---|---|
| Throughput | ~50K req/s | 100K+ req/s |
| Object pooling | Standar | Agresif (zero-allocation) |
| API style | Standar | Method-rich, non-standard |
| Kompabilitas middleware | 100% | Tidak kompatibel tanpa adaptor |
Type Handler |
http.Handler |
fasthttp.RequestHandler |
| Request/Response | http.Request, http.ResponseWriter |
fasthttp.RequestCtx |
| Body parsing | r.Body.Read() |
ctx.PostBody() |
| Header | r.Header.Get() |
ctx.Request.Header.Peek() |
| Query | r.URL.Query().Get() |
ctx.QueryArgs().Peek() |
Performa tinggi datang dengan harga: API non-standard, dan banyak middleware ekosistem Go (OpenTelemetry, prometheus, dll) tidak kompatibel langsung. Untuk kebanyakan aplikasi, net/http (atau Gin/Chi) sudah cukup. Pakai fasthttp hanya kalau kamu butuh throughput ekstrim.
Dockerfile Multi-Stage #
# 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"]
Dockerfile untuk Development #
# 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"]
Konfigurasi Air (.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:dev@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:
Contoh Aplikasi fasthttp #
Router Sederhana (internal/router/router.go):
fasthttp tidak punya router built-in, jadi kita tulis pattern matching sendiri. Untuk routing kompleks, tambahkan library github.com/fasthttp/router atau github.com/buaazp/fasthttprouter.
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)
}
Perhatikan beberapa perbedaan utama dengan net/http:
ctx.PostBody()untuk body, bukanr.Body.Read()ctx.UserValue("id")untuk path parameter, bukanchi.URLParam()ctx.QueryArgs().Peek("key")untuk query string, bukanr.URL.Query().Get("key")ctx.SetStatusCode()untuk set status, bukanw.WriteHeader()json.NewEncoder(ctx).Encode(...)langsung keRequestCtx
Middleware #
Middleware di fasthttp mengikuti pola func(fasthttp.RequestHandler) fasthttp.RequestHandler. Ini sedikit berbeda dari net/http yang pakai func(http.Handler) http.Handler.
Logger Middleware (internal/middleware/logger.go):
package middleware
import (
"log"
"time"
"github.com/valyala/fasthttp"
)
// Logger: log setiap request dengan method, path, status, dan 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 ke 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 dan set CORS header
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
}
}
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 dengan 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 manual)
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 untuk inject dependency (sesuaikan dengan struktur repository kamu)
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 punya banyak opsi tuning. Kunci performa ada di sini.
server := &fasthttp.Server{
Handler: handler,
Name: "myapp",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
MaxConnsPerIP: 1000, // max concurrent connection per IP
MaxRequestsPerConn: 1000, // close connection setelah N request
Concurrency: 10000, // max concurrent handler
ReadBufferSize: 4096, // buffer size untuk read
WriteBufferSize: 4096, // buffer size untuk write
DisableKeepalive: false, // keep-alive aktif
KeepHijackedConns: false, // close hijacked conns
TCPKeepalive: true, // TCP keep-alive
}
Untuk beban tinggi, naikkan Concurrency dan MaxConnsPerIP. Untuk API latency-sensitive, turunkan ReadTimeout dan WriteTimeout.
Routing Library Populer #
Bikin router sendiri itu pekerjaan yang berulang. Beberapa library yang bisa kamu pakai:
| Library | Kelebihan | Kekurangan |
|---|---|---|
fasthttp/router |
Resmi, simple, regex-based | Tidak mendukung parameter typed |
buaazp/fasthttprouter |
Lightweight, cepat | Tidak se-feature lengkap Go-chi |
@fiber/router (dari Fiber) |
Kaya fitur | Bergantung pada Fiber ecosystem |
| Custom trie | Performa tertinggi | Banyak kode sendiri |
Untuk kebanyakan kasus, github.com/buaazp/fasthttprouter cukup. Ia mendukung parameter :id, wildcard *, dan middleware.
Build dan Run #
# Build
docker compose build
# Jalankan
docker compose up -d
# Lihat log
docker compose logs -f api
# Benchmark (optional)
docker compose exec api wrk -t4 -c100 -d10s http://localhost:8080/health
# Stop
docker compose down
Akses:
- 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 user
curl http://localhost:8080/api/v1/users/1
Benchmark dengan wrk #
Untuk mengukur throughput, pakai wrk di dalam container atau di host.
# Install wrk di 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 untuk endpoint health sederhana. Untuk endpoint yang melakukan query database, throughput turun signifikan (tergantung latency DB).
Kapan fasthttp Tepat #
Pakai fasthttp langsung jika:
✓ Butuh throughput ekstrim (>50K req/s)
✓ Punya expertise untuk handle API non-standard
✓ Microservice high-throughput (gateway, proxy)
✓ Benchmark menunjukkan net/http jadi bottleneck
Pakai framework di atas fasthttp (Fiber, Gear) jika:
✓ Suka API Express-like
✓ Butuh ekosistem middleware
✓ Produktivitas lebih penting dari kontrol rendah
Pakai net/http / Gin / Chi jika:
✓ Aplikasi bisnis biasa
✓ Butuh kompatibilitas middleware ekosistem
✓ Tim Go baru (idiomatic Go lebih mudah dipelajari)
✓ Throughput 5-20K req/s sudah cukup
Untuk 90% aplikasi web, net/http atau Gin sudah lebih dari cukup. fasthttp unggul di use case spesifik: API gateway, real-time proxy, atau service yang memang di-benchmark untuk throughput tinggi.
Best Practice #
Cache Dependency Module #
Copy go.mod + go.sum dulu, baru source code. Build inkremental super cepat.
Pakai Air untuk Hot Reload #
Tanpa hot reload, Go terasa lambat. Air memantau file dan rebuild otomatis.
Hati-hati dengan Object Lifecycle #
fasthttp menggunakan RequestCtx yang di-reuse. Jangan simpan reference ke ctx setelah handler return. Untuk data yang perlu persist, copy ke struct terpisah.
Tuning Server untuk Beban #
Concurrency, MaxConnsPerIP, ReadTimeout, WriteTimeout — atur sesuai workload. Default cukup untuk kebanyakan kasus, tapi untuk high-throughput, naikkan Concurrency ke 10K+.
Pakai Connection Pool untuk Database #
GORM pakai database/sql di belakang layar. Set MaxOpenConns, MaxIdleConns, ConnMaxLifetime sesuai beban.
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)
Healthcheck dengan DB Ping #
/healthz harus cek dependency. Return 503 kalau database down. /readyz return 200 kalau app siap.
Pakai Library Routing yang Matang #
Jangan tulis router sendiri kecuali kamu butuh fitur khusus. fasthttp/router atau buaazp/fasthttprouter sudah cukup.
Bind Mount Source Code di Dev #
Mount source code ke container. Air akan rebuild otomatis setiap save.
Troubleshooting #
Memory Leak #
fasthttp pakai object pooling. Kalau handler kamu menyimpan reference ke RequestCtx atau Response, itu akan ditahan di pool dan tidak pernah di-GC. Selalu copy data yang perlu persist.
Connection Refused #
Pakai depends_on: condition: service_healthy di Compose. Tambahkan retry loop di kode Go.
Server Timeout #
Naikkan ReadTimeout dan WriteTimeout di fasthttp.Server. Default 10s cukup untuk API normal, tapi untuk upload file besar, naikkan ke 60s+.
Routing Tidak Match #
fasthttprouter tidak mendukung method override atau implicit HEAD. Definisikan route eksplisit untuk method yang kamu butuh. Pakai r.GET, r.POST, dst.
CORS Preflight Gagal #
Set r.GlobalOPTIONS = middleware.CORSHandler agar preflight OPTIONS di-handle secara global.
Ringkasan #
- fasthttp ideal untuk service Go yang butuh throughput ekstrim (>50K req/s).
- fasthttp pakai API non-standard —
RequestCtxalih-alihhttp.Request/ResponseWriter. Tidak kompatibel langsung dengan middleware ekosistem Go.- Dockerfile multi-stage untuk production: builder stage dengan Go toolchain, runtime stage ramping. Image akhir < 20 MB.
- Dockerfile.dev untuk development: full toolchain + Air untuk hot reload.
- Hot reload dengan Air: pantau file
.go, rebuild, restart. Konfigurasi lewat.air.toml.- Layer cache Go: copy
go.mod+go.sumdulu, jalankango mod download, baru copy source.- Middleware mengikuti pola
func(fasthttp.RequestHandler) fasthttp.RequestHandler. Compose manual:Recover(Logger(handler)).- Routing library:
github.com/buaazp/fasthttprouteruntuk kebanyakan kasus. Pakaifasthttp/routerresmi untuk yang lebih sederhana.- Server tuning:
Concurrency,MaxConnsPerIP,ReadTimeout,WriteTimeout. Tune sesuai workload.- Hati-hati object lifecycle:
RequestCtxdi-reuse. Jangan simpan reference setelah handler return. Copy data yang perlu persist.- Connection pool untuk database: set
MaxOpenConns,MaxIdleConns,ConnMaxLifetime.- Healthcheck cek dependency. Return 503 kalau database down.
- Benchmark dengan wrk untuk verifikasi throughput:
wrk -t4 -c100 -d10s http://localhost:8080/health.- Pakai fasthttp jika butuh throughput ekstrim atau microservice high-performance. Hindari jika API standar sudah cukup atau butuh ekosistem middleware.
- Best practice: cache dependency, hot reload, hati-hati object lifecycle, tuning server, connection pool, healthcheck.
- Alternatif: Fiber untuk API Express-like di atas fasthttp, Gin untuk kompatibilitas net/http, Chi untuk router ringan.