hibiken/asynq · error

asynq: the server is in the stopped state. Waiting for…

Error message

asynq: the server is in the stopped state. Waiting for shutdown.

What it means

Server.start checks the server state machine: when the state is Stopped (Stop was called, but the server has not been ShutDown/closed), a new Start is rejected with this error because the server is draining and awaiting final shutdown.

Solutions

  1. After Stop, the only valid next steps are ShutDown (graceful) — plan restarts by creating a new Server instance.
  2. Replace Stop-then-Start patterns with Stop for graceful drain followed by full ShutDown and server re-creation.
  3. Track your own state flag so Start is never called while draining.
  4. If you only need to temporarily halt processing, gate your handler logic instead of using Stop.

Example fix

// before
srv.Stop()
srv.Start(handler) // error: stopped state
// after
srv.Stop()
srv.ShutDown()
// create a fresh server to restart
srv2 := asynq.NewServer(redisOpt, cfg)
srv2.Start(handler)
Defensive patterns

Strategy: validation

Validate before calling

type ServerState int
const (StateNew ServerState = iota; StateRunning; StateStopped; StateClosed)
if state == StateStopped {
    return errors.New("server is stopping; cannot Start until ShutDown completes")
}

Try / catch

if err := srv.Start(handler); err != nil {
    if strings.Contains(err.Error(), "stopped state") {
        return nil // draining; ignore
    }
    return err
}

Prevention

When it happens

Trigger: Calling srv.Start(handler) or srv.Run(handler) after srv.Stop() but before srv.ShutDown(); attempting to resume processing from the Stopped state.

Common situations: Pause/resume logic that assumes Stop is reversible; graceful-restart code calling Stop then immediately Start; signal handlers that call Stop and then try to bring the server back up.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07). Data as JSON: /api/errors/de6b4a155c1f1d8c. Report an issue: GitHub.

Appendix: source

Thrown at server.go:712

	srv.syncer.start(&srv.wg)
	srv.recoverer.start(&srv.wg)
	srv.forwarder.start(&srv.wg)
	srv.processor.start(&srv.wg)
	srv.janitor.start(&srv.wg)
	srv.aggregator.start(&srv.wg)
	return nil
}

// Checks server state and returns an error if pre-condition is not met.
// Otherwise it sets the server state to active.
func (srv *Server) start() error {
	srv.state.mu.Lock()
	defer srv.state.mu.Unlock()
	switch srv.state.value {
	case srvStateActive:
		return fmt.Errorf("asynq: the server is already running")
	case srvStateStopped:
		return fmt.Errorf("asynq: the server is in the stopped state. Waiting for shutdown.")
	case srvStateClosed:
		return ErrServerClosed
	}
	srv.state.value = srvStateActive
	return nil
}

// Shutdown gracefully shuts down the server.
// It gracefully closes all active workers. The server will wait for
// active workers to finish processing tasks for duration specified in Config.ShutdownTimeout.
// If worker didn't finish processing a task during the timeout, the task will be pushed back to Redis.
func (srv *Server) Shutdown() {
	srv.state.mu.Lock()
	if srv.state.value == srvStateNew || srv.state.value == srvStateClosed {
		srv.state.mu.Unlock()
		// server is not running, do nothing and return.
		return
	}

View on GitHub (pinned to d135f1439b)