fatedier/frp · warning

client control for run id [%s] is not running

Error message

client control for run id [%s] is not running

What it means

RegisterWorkConn holds ctl.lifecycleMu and requires ctl.state == controlStateRunning before queuing a work connection. If the control is still pending activation or already closing/closed, the work conn is refused and ownership stays with the caller.

Source

Thrown at server/control.go:326

	entry, ok := cm.lockCurrentRun(ctl.runID, false)
	if !ok {
		cm.mu.RLock()
		closed := cm.closed
		cm.mu.RUnlock()
		if closed {
			return fmt.Errorf("control manager is closed")
		}
		return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID)
	}
	defer entry.runMu.Unlock()
	if entry.ctl != ctl || entry.id != ctl.controlID {
		return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID)
	}

	ctl.lifecycleMu.Lock()
	defer ctl.lifecycleMu.Unlock()
	if ctl.state != controlStateRunning {
		return fmt.Errorf("client control for run id [%s] is not running", ctl.runID)
	}

	select {
	case ctl.workConnCh <- conn:
		ctl.xl.Debugf("new work connection registered")
		return nil
	default:
		ctl.xl.Debugf("work connection pool is full, discarding")
		return fmt.Errorf("work connection pool is full, discarding")
	}
}

func (cm *ControlManager) Close() error {
	cm.mu.Lock()
	cm.closed = true
	ctls := make([]*Control, 0, len(cm.ctlsByRunID))
	for _, entry := range cm.ctlsByRunID {
		ctls = append(ctls, entry.ctl)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Verify frpc and frps versions match; older/patched clients that fire work connections eagerly can hit the pending window.
  2. Let frpc retry: the next ReqWorkConn issued after the control reaches running will succeed.
  3. If embedding, drop the work conn on this error — retrying against a non-running control is pointless.
Defensive patterns

Strategy: validation

Validate before calling

// wait for running state before sending work conns (client side: wait for LoginResp)
// server side embedding:
select {
case <-ctl.runningCh: // or poll state under lifecycleMu
case <-time.After(time.Second):
    return errors.New("control not running yet")
}

Try / catch

if err := cm.RegisterWorkConn(ctl, conn); err != nil { conn.Close() } // never retry a non-running control

Prevention

When it happens

Trigger: frpc delivers a NewWorkConn while the control is in controlStatePending (login handshake/activation not finished) or during shutdown after the state moved past running. RegisterWorkConn's state check fails and the error is returned.

Common situations: frpc sending work connections before it received LoginResp (protocol violation or eager optimisation); work connections arriving concurrently with control close (server shutdown, heartbeat timeout, proxy errors); races during handoff between control generations.

Related errors


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