fatedier/frp · error
control is already closed
Error message
control is already closed
What it means
In Control.GetWorkConn, when the pool is empty the server sends ReqWorkConn to frpc; if msgDispatcher.Send fails, the control's message stream is already broken (connection closed or dispatcher shut down), so it reports 'control is already closed'. Compare with the ErrCtlClosed sentinel returned when the workConnCh channel itself is closed.
Source
Thrown at server/control.go:616
if err := recover(); err != nil {
xl.Errorf("panic error: %v", err)
xl.Errorf(string(debug.Stack()))
}
}()
var ok bool
// get a work connection from the pool
select {
case workConn, ok = <-ctl.workConnCh:
if !ok {
err = pkgerr.ErrCtlClosed
return
}
xl.Debugf("get work connection from pool")
default:
// no work connections available in the poll, send message to frpc to get more
if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil {
return nil, fmt.Errorf("control is already closed")
}
select {
case workConn, ok = <-ctl.workConnCh:
if !ok {
err = pkgerr.ErrCtlClosed
xl.Warnf("no work connections available, %v", err)
return
}
case <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second):
err = fmt.Errorf("timeout trying to get work connection")
xl.Warnf("%v", err)
return
}
}
// When we get a work connection from pool, replace it with a new one.View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Let frpc's reconnect loop re-establish the control; user connections during the gap will fail and callers should retry.
- Investigate why the control dropped: check frpc logs for crashes/OOM, heartbeat timeout settings, and NAT/firewall idle timeouts.
- Tune transport.heartbeatTimeout and enable transport.tcpMux to keep the control connection alive.
Example fix
# frpc.toml — keep control connection healthy [transport] heartbeatTimeout = 90 tcpMux = true
Defensive patterns
Strategy: retry
Try / catch
if _, err := ctl.GetWorkConn(); err != nil {
if errors.Is(err, pkgerr.ErrCtlClosed) || strings.Contains(err.Error(), "control is already closed") {
// control gone: fail the user conn; frpc reconnect will restore service
return err
}
} Prevention
- Keep the control connection healthy: tcpMux, heartbeatTimeout tuned above NAT idle timeouts.
- Monitor frpc liveness so user traffic is not routed to a dead control.
When it happens
Trigger: A user connection arrives, the work conn pool is empty, and the control connection to frpc has just died (network drop, frpc exit, server closing the control). Send(&msg.ReqWorkConn{}) errors and the wrapper converts it to this error.
Common situations: frpc process crashing or restarting while user traffic hits the proxy; heartbeat timeout closing the control; NAT idle timeout dropping the control TCP connection.
Related errors
- client control for run id [%s] is no longer current
- client control for run id [%s] is not running
- work connection pool is full, discarding
- exec configuration is required when type is 'exec'
- file path cannot be empty
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/f7797775bb59626e.
Report an issue: GitHub.