fatedier/frp · error

invalid transport.heartbeatTimeout, heartbeat timeout should

Error message

invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval

What it means

frpc rejects a transport config where heartbeatTimeout is strictly less than heartbeatInterval (both explicitly set > 0). A timeout shorter than the interval means the client would time out before the next heartbeat is even sent, guaranteeing spurious disconnects.

Source

Thrown at pkg/config/v1/validation/client.go:151

		if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
			errs = AppendError(errs, err)
		}
	}
	if err := c.TokenSource.Validate(); err != nil {
		errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
	}
	return errs
}

func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
	var (
		warnings Warning
		errs     error
	)

	if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
		if c.HeartbeatTimeout < c.HeartbeatInterval {
			errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
		}
	}

	if !lo.FromPtr(c.TLS.Enable) {
		checkTLSConfig := func(name string, value string) Warning {
			if value != "" {
				return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
			}
			return nil
		}

		warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
		warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
		warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
	}

	if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
		errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Set transport.heartbeatTimeout >= transport.heartbeatInterval (e.g. interval 30, timeout 90)
  2. Or delete one of the two keys so the default ratio applies
  3. Double-check units — frp durations in TOML config are seconds unless specified

Example fix

# before
[transport]
heartbeatInterval = 90
heartbeatTimeout = 30

# after
[transport]
heartbeatInterval = 30
heartbeatTimeout = 90
Defensive patterns

Strategy: validation

Validate before calling

func heartbeatOK(t, i int) bool {
    return t <= 0 || i <= 0 || t >= i
}

Prevention

When it happens

Trigger: transport.heartbeatTimeout = 30 and transport.heartbeatInterval = 90 (units: seconds/milliseconds per config docs) — any positive pair with timeout < interval triggers it. Only fires when both are > 0.

Common situations: Tuning heartbeat values independently while debugging NAT keepalives; unit confusion (seconds vs ms) making the interval unexpectedly large; copying interval from one config and timeout from another.

Understand the failure class

Related errors


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