Graceful shutdown HTTP-сервера без сюрпризов

12 октября 2026

Почти в каждом сервисе есть код, который ловит SIGTERM и вызывает srv.Shutdown. Выглядит надёжно, но закрывает только HTTP-часть: фоновые воркеры, консьюмеры и пулы соединений продолжают жить своей жизнью.

Базовый вариант

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

go func() {
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}()

<-ctx.Done()

shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
    log.Printf("shutdown: %v", err)
}

Таймаут для Shutdown берётся от context.Background(), а не от уже отменённого ctx. Иначе сервер закроется мгновенно и оборвёт активные запросы.

Что обычно забывают

Воркеры, запущенные через go worker(ctx), получают сигнал отмены, но main их не ждёт. Процесс завершается, пока воркер дописывает данные. Я запускаю все долгоживущие горутины через errgroup, а в main делаю g.Wait() после остановки сервера.

И порядок важен: сначала перестаём принимать новые запросы, потом дожидаемся воркеров и только после этого закрываем базу и брокер.