charmbracelet/crush · error · ErrServerShuttingDown

server is shutting down

Error message

server is shutting down

What it means

ErrServerShuttingDown (client package) reports that the server refused a request because it has already committed to exiting. checkStatus maps HTTP 503 responses to this sentinel, and admitLocked enforces it server-side. The work is not lost: a replacement server can be started and the request retried against it.

Source

Thrown at internal/client/errors.go:30

var (
	// ErrNotFound reports that the server answered 404. For a
	// workspace-scoped call this means the server no longer knows the
	// workspace — it was torn down, or the server was replaced under the
	// client — so the right response is to re-register rather than retry
	// the same ID, which can never start succeeding again.
	ErrNotFound = errors.New("not found")

	// ErrServerBusy reports that the server declined to shut down
	// because it is still hosting workspaces or is midway through
	// creating one. A client asking a version-mismatched server to stand
	// down must keep using it instead of assuming it is going away.
	ErrServerBusy = errors.New("server busy")

	// ErrServerShuttingDown reports that the server refused the request
	// because it has already committed to exiting. The work is not lost:
	// a replacement server can be started and the request retried
	// against it.
	ErrServerShuttingDown = errors.New("server is shutting down")

	// ErrUnsupported reports that the running server does not understand
	// the request because it predates the feature. Callers must decide
	// what is safe to do with an older server rather than treating the
	// failure as transient.
	ErrUnsupported = errors.New("unsupported by the running server")
)

// checkStatus returns nil when rsp's status code is one of ok
// (http.StatusOK when none are given). Otherwise it returns an error
// carrying the status code and, when the body decodes as a proto.Error,
// the server-provided message. Statuses that callers act on are wrapped
// in the matching sentinel. checkStatus may consume the response body.
func checkStatus(rsp *http.Response, ok ...int) error {
	if len(ok) == 0 {
		ok = []int{http.StatusOK}
	}
	if slices.Contains(ok, rsp.StatusCode) {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Start a replacement server and retry the request against it
  2. Match with errors.Is(err, client.ErrServerShuttingDown) and never retry the same server
  3. Raise CRUSH_SERVER_IDLE_TIMEOUT to widen the reuse window and avoid racing shutdown

Example fix

// before
ws, err := client.CreateWorkspace(ctx, path)
if err != nil { return err }
// after
ws, err := client.CreateWorkspace(ctx, path)
if errors.Is(err, client.ErrServerShuttingDown) {
    if err := client.StartReplacementServer(ctx); err != nil { return err }
    ws, err = client.CreateWorkspace(ctx, path)
}
return err
Defensive patterns

Strategy: fallback

Type guard

func isErrServerShuttingDown(err error) bool {
    return errors.Is(err, client.ErrServerShuttingDown)
}

Try / catch

rsp, err := client.do(req)
if err != nil {
    if errors.Is(err, client.ErrServerShuttingDown) {
        return retryOnFreshServer(ctx, req) // 503 -> replacement server
    }
    return err
}

Prevention

When it happens

Trigger: Any client call whose response carries http.StatusServiceUnavailable (checkStatus); createWorkspaceOnLiveServer when the live server is tearing down; admitLocked rejecting a request after shutdown began; runSubscription hitting a 503 mid-stream setup.

Common situations: Reusing a server inside its idle shutdown window (DefaultIdleShutdownDelay); a client attaching exactly as the previous server exits; CI races where one step stops the server while another still talks to it.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/d0950f6c93214a28. Report an issue: GitHub.