hibiken/asynq · error

asynq: the scheduler is already running

Error message

asynq: the scheduler is already running

What it means

Scheduler.start rejects the transition when the scheduler's state is already srvStateActive, i.e. Start (or Run) was called while the scheduler is running. A Scheduler can only be started once from the idle state; starting an active one is a state-machine violation.

Solutions

  1. Call Start/Run exactly once per Scheduler; guard with sync.Once if multiple code paths may start it
  2. Create a new Scheduler instance if you need a fresh lifecycle
  3. Check s.State() before starting if your code can race

Example fix

// before
srv.Start(); srv.Run(mux) // second start
// after
var once sync.Once
once.Do(func(){ srv.Run(mux) })
Defensive patterns

Strategy: try-catch

Type guard

func canStart(s *asynq.Scheduler) bool {
    return s != nil && s.State() == asynq.StateNew // not yet active
}

Try / catch

var startOnce sync.Once
startOnce.Do(func() {
    if err := s.Run(mux); err != nil {
        log.Fatalf("scheduler start failed: %v", err)
    }
})

Prevention

When it happens

Trigger: Calling s.Start() or s.Run() twice on the same Scheduler instance without shutting it down; concurrent Start calls racing.

Common situations: Double invocation during app bootstrap (e.g. both an init path and a main path start the scheduler); re-running Run after temporarily stopping without Shutdown handling; test harnesses sharing a scheduler.

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

Appendix: source

Thrown at scheduler.go:278

	if err := s.start(); err != nil {
		return err
	}
	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()

View on GitHub (pinned to d135f1439b)