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

  1. If you operate the deployment: let frps finish shutting down; frpc will reconnect to the new instance automatically via its reconnect loop.
  2. 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.
  3. Add a shutdown health gate in your login handler that rejects new sessions before constructing a Control.
  4. 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

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


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/d89bff4949d5def0. Report an issue: GitHub.