plandex-ai/plandex · warning
failed to signal LiteLLM for shutdown: %w
Error message
failed to signal LiteLLM for shutdown: %w
What it means
During graceful shutdown, sending os.Interrupt to the LiteLLM proxy process failed. ShutdownLiteLLMServer returns this wrapped error immediately without waiting for the process to exit. Typically means the process handle is invalid or the process already died.
Source
Thrown at app/server/model/litellm.go:67
case <-ticker.C:
if isLiteLLMHealthy() {
log.Println("LiteLLM proxy is healthy")
return
} else {
log.Println("LiteLLM proxy is not healthy yet, retrying after 500ms...")
}
}
}
})
return finalErr
}
func ShutdownLiteLLMServer() error {
if liteLLMCmd != nil && liteLLMCmd.Process != nil {
log.Println("Shutting down LiteLLM proxy gracefully...")
if err := liteLLMCmd.Process.Signal(os.Interrupt); err != nil {
return fmt.Errorf("failed to signal LiteLLM for shutdown: %w", err)
}
done := make(chan error, 1)
go func() {
done <- liteLLMCmd.Wait()
}()
select {
case <-time.After(5 * time.Second):
log.Println("LiteLLM proxy shutdown timed out, forcing kill")
return liteLLMCmd.Process.Kill()
case err := <-done:
return err
}
}
return nil
}
View on GitHub (pinned to e2d772072e)
Solutions
- Ignore the error if the process already exited — check ProcessState before signalling
- Guard against double shutdown with a sync.Once on ShutdownLiteLLMServer
- Fall back to Process.Kill() if Interrupt fails and termination is required
- Check earlier logs for a LiteLLM crash that would explain the dead process
- In containers, ensure the signal is permitted (proper PID 1 handling)
Example fix
// before
if err := liteLLMCmd.Process.Signal(os.Interrupt); err != nil {
return fmt.Errorf("failed to signal LiteLLM for shutdown: %w", err)
}
// after: tolerate already-exited process
if err := liteLLMCmd.Process.Signal(os.Interrupt); err != nil && liteLLMCmd.ProcessState == nil {
return fmt.Errorf("failed to signal LiteLLM for shutdown: %w", err)
} Defensive patterns
Strategy: type-guard
Validate before calling
if liteLLMCmd == nil || liteLLMCmd.Process == nil || liteLLMCmd.ProcessState != nil {
return nil // already shut down or never started
} Type guard
func processAlive(cmd *exec.Cmd) bool {
return cmd != nil && cmd.Process != nil && cmd.ProcessState == nil
} Try / catch
if err := ShutdownLiteLLMServer(); err != nil {
if processAlive(liteLLMCmd) {
_ = liteLLMCmd.Process.Kill()
}
} Prevention
- Wrap shutdown in sync.Once to prevent double-signal races
- Check ProcessState before signalling a possibly-dead process
- Fall back to Kill when Interrupt fails
- Log LiteLLM crashes so dead-process signals are expected, not surprising
When it happens
Trigger: liteLLMCmd.Process.Signal(os.Interrupt) returns an error: process already exited, process finished between the nil check and Signal, or OS-level permission problem signaling the child.
Common situations: Proxy crashed earlier (so Signal targets a zombie/dead PID); concurrent shutdown calls racing on liteLLMCmd; container runtime restricting signals; PID reuse edge cases.
Related errors
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/20c8cd0f8504d217.
Report an issue: GitHub.