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

  1. Ignore the error if the process already exited — check ProcessState before signalling
  2. Guard against double shutdown with a sync.Once on ShutdownLiteLLMServer
  3. Fall back to Process.Kill() if Interrupt fails and termination is required
  4. Check earlier logs for a LiteLLM crash that would explain the dead process
  5. 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

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.