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

  1. Set heartbeat_timeout to at least heartbeat_interval (conventionally 3x, e.g. interval=30, timeout=90)
  2. Or remove heartbeat_timeout entirely and let frpc apply its default (90s), which is already > the default interval
  3. 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

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

Related errors


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