rathole-org/rathole · error

Heartbeat timed out

Error message

Heartbeat timed out

What it means

Fires in the client's control-channel event loop when read_control_cmd does not produce a command within the heartbeat timeout: the server stopped sending heartbeats or the control connection stalled/died. The loop's select! arm for the heartbeat interval elapses, so the client treats the control channel as lost and tears it down so the connection can be re-established.

Solutions

  1. Check network stability and NAT/firewall idle timeouts between client and server
  2. Increase heartbeat interval/timeout constants if links are high-latency
  3. Ensure the server process is alive and not blocked; inspect server logs for the same period
  4. Rely on the client's reconnection/backoff logic (run_control_chan_backoff) to restore the channel
Defensive patterns

Strategy: retry

When it happens

Trigger: Thrown at src/client.rs:484 when the library encounters an invalid state.

Common situations: See trigger scenarios.

Understand the failure class


AI-assisted analysis of rathole-org/rathole@a292f7ed54 (2026-09-07). Data as JSON: /api/errors/fcad56dbe386f197. Report an issue: GitHub.

Appendix: source

Thrown at src/client.rs:484

        loop {
            tokio::select! {
                val = read_control_cmd(&mut conn) => {
                    let val = val?;
                    debug!( "Received {:?}", val);
                    match val {
                        ControlChannelCmd::CreateDataChannel => {
                            let args = data_ch_args.clone();
                            tokio::spawn(async move {
                                if let Err(e) = run_data_channel(args).await.with_context(|| "Failed to run the data channel") {
                                    warn!("{:#}", e);
                                }
                            }.instrument(Span::current()));
                        },
                        ControlChannelCmd::HeartBeat => ()
                    }
                },
                _ = time::sleep(Duration::from_secs(self.heartbeat_timeout)), if self.heartbeat_timeout != 0 => {
                    return Err(anyhow!("Heartbeat timed out"))
                }
                _ = &mut self.shutdown_rx => {
                    break;
                }
            }
        }

        info!("Control channel shutdown");
        Ok(())
    }
}

impl ControlChannelHandle {
    #[instrument(name="handle", skip_all, fields(service = %service.name))]
    fn new<T: 'static + Transport>(
        service: ClientServiceConfig,
        remote_addr: String,
        transport: Arc<T>,

View on GitHub (pinned to a292f7ed54)