fatedier/frp · error
invalid heartbeat_timeout, heartbeat_timeout is less than he
Error message
invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval
What it means
This error comes from ClientCommonConf.Validate in the legacy INI frpc config parser. It rejects a [common] section where both heartbeat_interval and heartbeat_timeout are set to positive values but heartbeat_timeout is smaller than heartbeat_interval. Since the timeout must give the heartbeat enough time to arrive, a timeout shorter than the interval is logically invalid.
Source
Thrown at pkg/config/legacy/client.go:365
// values of the new configuration.
func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{
ClientConfig: legacyauth.GetDefaultClientConf(),
TCPMux: true,
LoginFailExit: true,
Protocol: "tcp",
Start: make([]string, 0),
TLSEnable: true,
DisableCustomTLSFirstByte: true,
Metas: make(map[string]string),
IncludeConfigFiles: make([]string, 0),
}
}
func (cfg *ClientCommonConf) Validate() error {
if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
}
}
if !cfg.TLSEnable {
if cfg.TLSCertFile != "" {
fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
}
if cfg.TLSKeyFile != "" {
fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false")
}
if cfg.TLSTrustedCaFile != "" {
fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false")
}
}
if !slices.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Set heartbeat_timeout to at least heartbeat_interval (conventionally 3x, e.g. interval=30, timeout=90)
- Or remove heartbeat_timeout entirely and let frpc apply its default (90s), which is already > the default interval
- If migrating to the new TOML/YAML format, use transport.heartbeatInterval and transport.heartbeatTimeout with correct ordering
Example fix
# before [common] heartbeat_interval = 10 heartbeat_timeout = 3 # after [common] heartbeat_interval = 10 heartbeat_timeout = 30
Defensive patterns
Strategy: validation
Validate before calling
// before calling load: check INI values
iv, _ := strconv.Atoi(common.HeartbeatInterval)
tv, _ := strconv.Atoi(common.HeartbeatTimeout)
if tv > 0 && iv > 0 && tv < iv {
return fmt.Errorf("heartbeat_timeout (%d) must be >= heartbeat_interval (%d)", tv, iv)
} Type guard
func validHeartbeat(interval, timeout int) bool {
return timeout == 0 || interval == 0 || timeout >= interval
} Try / catch
if err := legacy.LoadConfigureFromFile(path, ...); err != nil { if strings.Contains(err.Error(), "invalid heartbeat_timeout") { /* fix interval/timeout values, notify operator */ } return err } Prevention
- Always derive heartbeat_timeout from heartbeat_interval (e.g. 3x) in config templates
- Run `frpc verify -c frpc.ini` in CI before deploy
- Prefer the new TOML format and set transport.heartbeatTimeout explicitly
When it happens
Trigger: A legacy INI frpc.ini containing e.g. heartbeat_interval = 10 and heartbeat_timeout = 3. Both values must be > 0 for the check to run; if either is 0 (unset), validation is skipped.
Common situations: Users copying heartbeat examples from old tutorials, or tuning heartbeat_interval up (e.g. to reduce traffic) while leaving an old heartbeat_timeout in place; also seen after migrating configs between frp versions where defaults changed (defaults are interval 30 / timeout 90).
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/266186096e9c6da3.
Report an issue: GitHub.