caddyserver/caddy · warning
server graceful shutdown %ds timeout
Error message
server graceful shutdown %ds timeout
What it means
Not a returned error by itself: when apps.grace_period > 0, the shutdown context gets this message as its cause via context.WithTimeoutCause. If graceful shutdown exceeds the grace period, in-flight handlers see context cancellation whose Cause() is 'server graceful shutdown Ns timeout'. Active connections are then forcibly closed.
Source
Thrown at modules/caddyhttp/app.go:744
}
}
}
}
}
// honor scheduled/delayed shutdown time
if delay {
app.logger.Info("shutdown scheduled",
zap.Duration("delay_duration", time.Duration(app.ShutdownDelay)),
zap.Time("time", scheduledTime))
time.Sleep(time.Duration(app.ShutdownDelay))
}
// enforce grace period if configured
if app.GracePeriod > 0 {
var cancel context.CancelFunc
timeout := time.Duration(app.GracePeriod)
ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds())))
defer cancel()
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout))
} else {
app.logger.Info("servers shutting down with eternal grace period")
}
// goroutines aren't guaranteed to be scheduled right away,
// so we'll use one WaitGroup to wait for all the goroutines
// to start their server shutdowns, and another to wait for
// them to finish; we'll always block for them to start so
// that when we return the caller can be confident* that the
// old servers are no longer accepting new connections
// (* the scheduler might still pause them right before
// calling Shutdown(), but it's unlikely)
var startedShutdown, finishedShutdown sync.WaitGroup
// these will run in goroutines
stopServer := func(server *Server) {View on GitHub (pinned to 50e54ee279)
Solutions
- Increase apps.http.grace_period (duration string, e.g. "30s" or "2m") to cover your longest expected request
- Make clients reconnect gracefully (websocket retry) so forced closes are harmless
- Use shutdown_delay to drain: stop accepting after a delay before the grace clock starts
Example fix
// before
"apps":{"http":{"grace_period":"5s"}}
// after
"apps":{"http":{"grace_period":"60s"}} Defensive patterns
Strategy: try-catch
Try / catch
// in your shutdown orchestration
select {
case <-ctx.Done():
if cause := context.Cause(ctx); cause != nil && strings.Contains(cause.Error(), "graceful shutdown") {
// grace period expired; in-flight connections were cut — reconnect clients
}
} Prevention
- Set grace_period above your p99 request/websocket session length
- Use shutdown_delay to let load balancers drain before the grace clock starts
- Implement client-side reconnect with backoff for long-lived connections
When it happens
Trigger: Shutdown (SIGTERM/SIGINT or config reload) while long-lived connections (websockets, large downloads, slow clients) exceed the configured grace_period (apps.http.grace_period, e.g. 5s), causing the deadline to fire.
Common situations: Kubernetes/systemd stopping Caddy with active websocket clients; large file transfers cut off at the default/short grace period; frequent config reloads in front of slow mobile clients.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- %v; additionally, aborting app %s: %v
- method not allowed
- %s app module: start: %v
- %v; additionally, cleanup: %v
- provision %s: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/6cd410fc1ecd21a1.
Report an issue: GitHub.