From 605ea3849f44047781a8d780886106ee403866f1 Mon Sep 17 00:00:00 2001
From: Patrick Scheid
Date: Mon, 23 Feb 2026 00:07:45 +0100
Subject: [PATCH] Make graceful shutdown timeout configurable via
SHUTDOWN_TIMEOUT
The shutdown timeout was hardcoded to 10s. Long-running webhook
processing or slow database drains could get cut off. Add a
SHUTDOWN_TIMEOUT env var (default: 10s) following the existing
SESSION_MAX_AGE pattern.
---
.env.example | 1 +
cmd/server/main.go | 7 +++----
internal/platform/config/config.go | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.env.example b/.env.example
index 644166d..a44db87 100644
--- a/.env.example
+++ b/.env.example
@@ -43,3 +43,4 @@ LOG_FORMAT=text
# WebSocket / Session
MAX_WEBSOCKET_CONNECTIONS=10000 # Used to check file descriptor limit at startup
SESSION_MAX_AGE=168h # HTTP session cookie expiry (7 days)
+SHUTDOWN_TIMEOUT=10s # Graceful shutdown deadline (default: 10s)
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 8b4048a..f9d1c2b 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -35,7 +35,6 @@ const (
dbConnectTimeout = 30 * time.Second
migrationTimeout = 60 * time.Second
eventsubSetupTimeout = 30 * time.Second
- shutdownTimeout = 10 * time.Second
configCacheTTL = 10 * time.Second
configEvictInterval = 1 * time.Minute
)
@@ -168,7 +167,7 @@ func initWebhookHandler(cfg *config.Config, ovl *app.Overlay, presence twitch.Vi
return twitch.NewWebhookHandler(cfg.WebhookSecret, ovl, presence, publisher, onVoteApplied, voteMetrics)
}
-func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node) <-chan struct{} {
+func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node, timeout time.Duration) <-chan struct{} {
done := make(chan struct{})
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
@@ -177,7 +176,7 @@ func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node) <-chan s
<-sigChan
slog.Info("Shutdown signal received, cleaning up...")
- ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
@@ -273,7 +272,7 @@ func main() {
os.Exit(1)
}
- done := runGracefulShutdown(srv, ws.node)
+ done := runGracefulShutdown(srv, ws.node, cfg.ShutdownTimeout)
if err := srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("Server error", "error", err)
diff --git a/internal/platform/config/config.go b/internal/platform/config/config.go
index 06fbcbb..f5d86d0 100644
--- a/internal/platform/config/config.go
+++ b/internal/platform/config/config.go
@@ -31,7 +31,8 @@ type Config struct {
MaxWebSocketConnections int `env:"MAX_WEBSOCKET_CONNECTIONS" default:"10000"`
- SessionMaxAge time.Duration `env:"SESSION_MAX_AGE" default:"168h"` // 7 days
+ SessionMaxAge time.Duration `env:"SESSION_MAX_AGE" default:"168h"` // 7 days
+ ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" default:"10s"` // Graceful shutdown deadline
}
func Load() (*Config, error) {