Go Fiber graceful shutdown dengan context
Graceful shutdown Fiber: tunggu request inflight selesai, close DB pool, drain message queue. Tanpa interrupt user di tengah transaksi.
Dipublikasikan 22 Juni 2026
Deploy Fiber service tanpa graceful shutdown = request inflight kepotong, transaksi tergantung setengah jalan. Pattern shutdown yang bener: tangkap SIGTERM, stop terima request baru, tunggu yang lagi diproses selesai, close resource. Snippet ini full stack — HTTP, DB, NATS consumer.
Kode
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/nats-io/nats.go"
)
// Server bungkus semua dependency
type Server struct {
app *fiber.App
db *pgxpool.Pool
nc *nats.Conn
logger *slog.Logger
}
func NewServer(logger *slog.Logger) (*Server, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
db, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
return nil, fmt.Errorf("db: %w", err)
}
nc, err := nats.Connect(os.Getenv("NATS_URL"),
nats.MaxReconnects(-1),
nats.ReconnectWait(2*time.Second),
)
if err != nil {
db.Close()
return nil, fmt.Errorf("nats: %w", err)
}
app := fiber.New(fiber.Config{
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
})
s := &Server{app: app, db: db, nc: nc, logger: logger}
s.routes()
return s, nil
}
func (s *Server) routes() {
s.app.Get("/healthz", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
s.app.Post("/order", func(c fiber.Ctx) error {
// Pakai context dari request — kalau client cancel, propagate
ctx := c.Context()
var order struct {
ProdukID int `json:"produk_id"`
Qty int `json:"qty"`
}
if err := c.Bind().JSON(&order); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
var orderID int
err := s.db.QueryRow(ctx,
"INSERT INTO orders (produk_id, qty) VALUES ($1, $2) RETURNING id",
order.ProdukID, order.Qty,
).Scan(&orderID)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"order_id": orderID})
})
}
// startConsumer subscribe NATS dan return cancel function
func (s *Server) startConsumer(ctx context.Context) (func(), error) {
sub, err := s.nc.Subscribe("order.created", func(m *nats.Msg) {
s.logger.Info("processing order event", "data", string(m.Data))
// proses event (idempotent)
time.Sleep(500 * time.Millisecond)
_ = m.Ack()
})
if err != nil {
return nil, err
}
cancel := func() {
if err := sub.Drain(); err != nil {
s.logger.Error("drain subscription gagal", "err", err)
}
}
return cancel, nil
}
func (s *Server) Run(ctx context.Context, addr string) error {
cancelConsumer, err := s.startConsumer(ctx)
if err != nil {
return err
}
// Start HTTP server di goroutine
serverErr := make(chan error, 1)
go func() {
s.logger.Info("HTTP listen", "addr", addr)
if err := s.app.Listen(addr); err != nil && !errors.Is(err, fiber.ErrGracefulShutdown) {
serverErr <- err
}
close(serverErr)
}()
// Tunggu signal atau ctx canceled
select {
case <-ctx.Done():
s.logger.Info("shutdown signal diterima, mulai graceful shutdown")
case err := <-serverErr:
return fmt.Errorf("server crash: %w", err)
}
// Shutdown sequence
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
s.logger.Info("stop terima request baru, tunggu inflight selesai")
if err := s.app.ShutdownWithContext(shutdownCtx); err != nil {
s.logger.Error("HTTP shutdown gagal", "err", err)
}
}()
go func() {
defer wg.Done()
s.logger.Info("drain NATS subscriber")
cancelConsumer()
s.nc.Close()
}()
go func() {
defer wg.Done()
s.logger.Info("close DB pool")
s.db.Close()
}()
wg.Wait()
s.logger.Info("graceful shutdown selesai")
return nil
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
server, err := NewServer(logger)
if err != nil {
logger.Error("init server gagal", "err", err)
os.Exit(1)
}
// Listen untuk SIGTERM (k8s) dan SIGINT (Ctrl+C)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
if err := server.Run(ctx, ":8080"); err != nil {
logger.Error("server run gagal", "err", err)
os.Exit(1)
}
}
Pemakaian
# Build & run
go run main.go
# Trigger graceful shutdown (Ctrl+C atau Kubernetes scale-down)
# Output:
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"shutdown signal diterima"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"stop terima request baru"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"drain NATS subscriber"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"close DB pool"}
# {"time":"2026-06-22T10:00:05Z","level":"INFO","msg":"graceful shutdown selesai"}
# k8s/deployment.yaml — penting set gracePeriod
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 30 # > shutdown timeout di code
containers:
- name: api
image: tokopedia-api:v1.0
lifecycle:
preStop:
exec:
# Delay 5s sebelum SIGTERM supaya Service endpoint terupdate dulu
command: ["/bin/sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /healthz
port: 8080
Kapan dipakai
- HTTP API production di Kubernetes / Cloud Run.
- Background worker yang consume message queue.
- Service yang punya DB transaction in-progress.
- Cron / scheduled job yang panjang.
Catatan
- signal.NotifyContext lebih clean dari channel manual — Go 1.16+.
- NATS Drain vs Close — Drain selesaikan pending message, Close paksa putus. Pakai Drain dulu, baru Close.
- DB pool Close — pgx pool tunggu inflight query selesai sebelum tutup. Aman dipanggil terakhir.
- preStop sleep 5s di k8s — beri waktu Service endpoint controller propagate “pod not ready” sebelum container start SIGTERM. Tanpa ini, ada window k8s tetap routing ke pod yang shutdown.
- terminationGracePeriodSeconds harus > shutdown timeout di code. Default k8s 30s, code di sini timeout 20s — aman.
- errors.Is fiber.ErrGracefulShutdown — Fiber return error khusus saat shutdown. Bukan error sebenarnya, exclude dari log error.
Test graceful shutdown locally: jalankan, kirim request long-running ke /order, terus Ctrl+C. Request harus selesai sukses, bukan EOF.
# tags
gofibergraceful-shutdownsignalcontext
Ditulis oleh Asti Larasati · 22 Juni 2026