slackhq/nebula · warning
ErrAlreadyStarted
ErrAlreadyStarted
Error message
nebula is already started
What it means
ErrAlreadyStarted in control.go means Control.Start() was called while the instance is already in StateStarted. The state machine in Start only accepts StateReady; StateStarted returns this error, StateStopped/Stopping return ErrAlreadyStopped, anything else returns ErrUnknownState.
Source
Thrown at control.go:28
"sync"
"syscall"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay"
)
type RunState int
const (
StateUnknown RunState = iota
StateReady
StateStarted
StateStopping
StateStopped
)
var ErrAlreadyStarted = errors.New("nebula is already started")
var ErrAlreadyStopped = errors.New("nebula cannot be restarted")
var ErrUnknownState = errors.New("nebula state is invalid")
// Every interaction here needs to take extra care to copy memory and not return or use arguments "as is" when touching
// core. This means copying IP objects, slices, de-referencing pointers and taking the actual value, etc
type controlEach func(h *HostInfo)
type controlHostLister interface {
QueryVpnAddr(vpnAddr netip.Addr) *HostInfo
ForEachIndex(each controlEach)
ForEachVpnAddr(each controlEach)
GetPreferredRanges() []netip.Prefix
}
type Control struct {
stateLock sync.Mutex
state RunStateView on GitHub (pinned to dd8f660c0a)
Solutions
- Check c.State() (or track lifecycle yourself) before calling Start.
- Treat ErrAlreadyStarted as a no-op success: use errors.Is(err, ErrAlreadyStarted) to skip instead of failing.
- Restructure startup to a single idempotent entry point guarded by sync.Once or a mutex.
- If a restart is intended, call Stop() first and wait for StateStopped.
Example fix
// before
c.Start()
...
c.Start() // panics flow with ErrAlreadyStarted
// after
if err := c.Start(); err != nil && !errors.Is(err, ErrAlreadyStarted) {
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if c.State() == control.StateStarted {
// already running; skip Start
return nil
} Type guard
func canStart(c *control.Control) bool {
return c.State() == control.StateReady
} Try / catch
if err := c.Start(); err != nil {
if errors.Is(err, control.ErrAlreadyStarted) {
return nil // idempotent
}
return err
} Prevention
- Route all Start calls through one idempotent supervisor function.
- Use sync.Once or a state flag to deduplicate start attempts.
- Check State() before Start in health-check/retry loops.
- Never treat ErrAlreadyStarted as fatal in double-start-prone environments like systemd.
When it happens
Trigger: Calling c.Start() twice without an intervening Stop (control.go:83); asserted in TestControl_StartStopLifecycle and control_lifecycle_test.go:277.
Common situations: Supervisor/systemd double-start attempts, retry logic that re-invokes Start on ambiguous failure, tests or hot-reload paths starting an already-running instance.
Related errors
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/c98782418f93b9d3.
Report an issue: GitHub.