hibiken/asynq · error

asynq: the scheduler has already been stopped

Error message

asynq: the scheduler has already been stopped

What it means

Scheduler.start rejects the transition when the scheduler's state is srvStateClosed, meaning Shutdown/Stop was already called. A closed Scheduler cannot be restarted; asynq exposes this as an explicit error rather than panicking.

Solutions

  1. Create a new Scheduler (NewScheduler(...)) after Shutdown instead of restarting the old one
  2. Restructure so Shutdown only happens at final teardown, not for temporary pauses (use Stop for pauses)
  3. Treat this error as a programming bug and fix the lifecycle ownership

Example fix

// before
srv.Shutdown(); srv.Start(h) // restart after close
// after
srv.Shutdown()
srv = asynq.NewScheduler(rconn, opts)
srv.Start(h)
Defensive patterns

Strategy: fallback

Validate before calling

if closed.Load() {
    return errors.New("scheduler already shut down; create a new instance")
}

Try / catch

if err := s.Start(h); err != nil {
    if strings.Contains(err.Error(), "already been stopped") {
        s = asynq.NewScheduler(conn, opts) // recreate
        return s.Start(h)
    }
    return err
}

Prevention

When it happens

Trigger: Calling s.Start()/s.Run() after s.Shutdown() (or s.Stop()+Shutdown) on the same instance; reuse of a long-lived scheduler across config reloads that shut it down.

Common situations: Graceful shutdown handlers that close the scheduler, followed by retry logic that tries to restart it; embedding the scheduler in a service manager that restarts components.

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/d0868f33415c107b. Report an issue: GitHub.

Appendix: source

Thrown at scheduler.go:280

	}
	s.logger.Info("Scheduler starting")
	s.logger.Infof("Scheduler timezone is set to %v", s.location)
	s.cron.Start()
	s.wg.Add(1)
	go s.runHeartbeater()
	return nil
}

// Checks server state and returns an error if pre-condition is not met.
// Otherwise it sets the server state to active.
func (s *Scheduler) start() error {
	s.state.mu.Lock()
	defer s.state.mu.Unlock()
	switch s.state.value {
	case srvStateActive:
		return fmt.Errorf("asynq: the scheduler is already running")
	case srvStateClosed:
		return fmt.Errorf("asynq: the scheduler has already been stopped")
	}
	s.state.value = srvStateActive
	return nil
}

// Shutdown stops and shuts down the scheduler.
func (s *Scheduler) Shutdown() {
	s.state.mu.Lock()
	if s.state.value == srvStateNew || s.state.value == srvStateClosed {
		// scheduler is not running, do nothing and return.
		s.state.mu.Unlock()
		return
	}
	s.state.value = srvStateClosed
	s.state.mu.Unlock()

	s.logger.Info("Scheduler shutting down")
	close(s.done) // signal heartbeater to stop

View on GitHub (pinned to d135f1439b)