fatedier/frp · error
control manager is closed
Error message
control manager is closed
What it means
Thrown by ControlManager.Add when the manager has been closed (cm.closed == true) while a new Control is being admitted. The control manager is the frps-side registry of client control connections keyed by run ID; once Close() runs (server shutdown), no new controls can be registered and Add fails immediately.
Source
Thrown at server/control.go:125
// Add makes ctl the pending current generation and records the predecessor
// finalization barrier it must wait for before activation.
func (cm *ControlManager) Add(ctl *Control) error {
for {
// Never wait for a run gate while holding cm.mu.
cm.mu.RLock()
old := cm.ctlsByRunID[ctl.runID]
cm.mu.RUnlock()
if old != nil {
old.runMu.Lock()
}
cm.mu.Lock()
if cm.closed {
cm.mu.Unlock()
if old != nil {
old.runMu.Unlock()
}
return fmt.Errorf("control manager is closed")
}
if cm.ctlsByRunID[ctl.runID] != old {
cm.mu.Unlock()
if old != nil {
old.runMu.Unlock()
}
continue
}
id := ControlID(nextControlID.Add(1))
if err := ctl.admit(cm, id); err != nil {
cm.mu.Unlock()
if old != nil {
old.runMu.Unlock()
}
return err
}
View on GitHub (pinned to 6c8a8d0a97)
Solutions
- If you operate the deployment: let frps finish shutting down; frpc will reconnect to the new instance automatically via its reconnect loop.
- If you embed frps as a library: stop accepting new login connections (close the listener) before calling ControlManager.Close(), so Add cannot race with shutdown.
- Add a shutdown health gate in your login handler that rejects new sessions before constructing a Control.
- In tests, synchronize Close() with in-flight Add() calls via a WaitGroup before asserting state.
Example fix
// before listener.Accept() // -> spawns login -> cm.Add(ctl) races with cm.Close() // after // stop accepting before closing the manager listener.Close() loginWG.Wait() cm.Close()
Defensive patterns
Strategy: validation
Validate before calling
// before spawning a login goroutine
if cm.IsClosed() { // expose or track shutdown state
return errors.New("server shutting down, reject login")
} Try / catch
// Go: treat as terminal for this login attempt
if err := cm.Add(ctl); err != nil {
if strings.Contains(err.Error(), "control manager is closed") {
// server draining: close conn, do not retry against this instance
conn.Close()
return
}
return err
} Prevention
- Close the login listener before closing the ControlManager during shutdown.
- Track in-flight logins with a WaitGroup and drain them before Close().
- In clients, rely on the built-in reconnect loop instead of immediate retries during server restarts.
When it happens
Trigger: Calling ControlManager.Add(ctl) after ControlManager.Close() has already set cm.closed = true. This happens when a frpc client logs in concurrently with frps shutdown: the login goroutine builds a Control and calls Add, but the shutdown path won the race and marked the manager closed.
Common situations: Rolling restarts or SIGTERM of frps while clients are reconnecting; aggressive reconnect loops from frpc hitting a server that is draining; tests that close the manager then feed it leftover login connections.
Related errors
- client control for run id [%s] is not running
- exec configuration is required when type is 'exec'
- file path cannot be empty
- exec command cannot be empty
- exec env name cannot be empty
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/d89bff4949d5def0.
Report an issue: GitHub.