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
- After Stop, the only valid next steps are ShutDown (graceful) — plan restarts by creating a new Server instance.
- Replace Stop-then-Start patterns with Stop for graceful drain followed by full ShutDown and server re-creation.
- Track your own state flag so Start is never called while draining.
- 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
- Treat Stop as one-way toward ShutDown; never call Start after Stop.
- Implement restarts as: ShutDown old server → create new Server → Start.
- Gate temporary pauses at the handler level instead of using Stop.
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
- asynq: the server is already running
- asynq: Server closed
- batch enqueue does not support group tasks
- batch enqueue does not support unique tasks
- redis connection is shared so the Inspector can't be closed…
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)