{"record":{"id":"6cd410fc1ecd21a1","repo":"caddyserver/caddy","slug":"server-graceful-shutdown-ds-timeout","errorCode":null,"errorMessage":"server graceful shutdown %ds timeout","messagePattern":"server graceful shutdown (.+?)s timeout","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"modules/caddyhttp/app.go","lineNumber":744,"sourceCode":"\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// honor scheduled/delayed shutdown time\n\tif delay {\n\t\tapp.logger.Info(\"shutdown scheduled\",\n\t\t\tzap.Duration(\"delay_duration\", time.Duration(app.ShutdownDelay)),\n\t\t\tzap.Time(\"time\", scheduledTime))\n\t\ttime.Sleep(time.Duration(app.ShutdownDelay))\n\t}\n\n\t// enforce grace period if configured\n\tif app.GracePeriod > 0 {\n\t\tvar cancel context.CancelFunc\n\t\ttimeout := time.Duration(app.GracePeriod)\n\t\tctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf(\"server graceful shutdown %ds timeout\", int(timeout.Seconds())))\n\t\tdefer cancel()\n\t\tapp.logger.Info(\"servers shutting down; grace period initiated\", zap.Duration(\"duration\", timeout))\n\t} else {\n\t\tapp.logger.Info(\"servers shutting down with eternal grace period\")\n\t}\n\n\t// goroutines aren't guaranteed to be scheduled right away,\n\t// so we'll use one WaitGroup to wait for all the goroutines\n\t// to start their server shutdowns, and another to wait for\n\t// them to finish; we'll always block for them to start so\n\t// that when we return the caller can be confident* that the\n\t// old servers are no longer accepting new connections\n\t// (* the scheduler might still pause them right before\n\t// calling Shutdown(), but it's unlikely)\n\tvar startedShutdown, finishedShutdown sync.WaitGroup\n\n\t// these will run in goroutines\n\tstopServer := func(server *Server) {","sourceCodeStart":726,"sourceCodeEnd":762,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules/caddyhttp/app.go#L726-L762","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n\"apps\":{\"http\":{\"grace_period\":\"5s\"}}\n// after\n\"apps\":{\"http\":{\"grace_period\":\"60s\"}}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// in your shutdown orchestration\nselect {\ncase <-ctx.Done():\n    if cause := context.Cause(ctx); cause != nil && strings.Contains(cause.Error(), \"graceful shutdown\") {\n        // grace period expired; in-flight connections were cut — reconnect clients\n    }\n}","preventionTips":["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"],"tags":["caddy","shutdown","grace-period","lifecycle","timeouts"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}