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

  1. Increase apps.http.grace_period (duration string, e.g. "30s" or "2m") to cover your longest expected request
  2. Make clients reconnect gracefully (websocket retry) so forced closes are harmless
  3. 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

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

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/6cd410fc1ecd21a1. Report an issue: GitHub.