zeroclaw-labs/zeroclaw · error
QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_
Error message
QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_MISSED_ACKS} consecutive missed ACKs) What it means
Raised by QQChannel::listen when MAX_MISSED_ACKS (3) consecutive heartbeats go unacknowledged (ExitReason::HeartbeatTimeout). The loop sends op 1 heartbeats on the interval negotiated with the gateway, increments missed_ack_count on each beat, and resets it on op 11 ACKs; only after 3 consecutive misses does it declare the connection zombied and bail. A WARN log with MAX_MISSED_ACKS as an attr precedes the bail, and Resume is attempted on the next listen().
Source
Thrown at crates/zeroclaw-channels/src/qq.rs:1810
}
ExitReason::StreamEnded => {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown),
"WebSocket stream ended unexpectedly; resume will be attempted on reconnect"
);
anyhow::bail!("QQ WebSocket connection closed: stream ended unexpectedly")
}
ExitReason::HeartbeatTimeout => {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown)
.with_attrs(::serde_json::json!({"MAX_MISSED_ACKS": MAX_MISSED_ACKS})),
"heartbeat timeout after consecutive missed ACKs; resume will be attempted on reconnect"
);
anyhow::bail!(
"QQ WebSocket connection closed: heartbeat ACK timeout \
({MAX_MISSED_ACKS} consecutive missed ACKs)"
)
}
ExitReason::WriteFailed => {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown),
"WebSocket write failed; resume will be attempted on reconnect"
);
anyhow::bail!("QQ WebSocket connection closed: write failed")
}
ExitReason::ChannelClosed => {
anyhow::bail!("QQ WebSocket connection closed: internal message channel closed")
}
}
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Rely on the reconnect: this exit exists precisely to replace zombie connections; Resume recovers the session
- If frequent, measure network loss to the gateway host and fix the underlying connectivity
- Avoid process suspension without reconnect logic — a paused process that wakes up almost always trips this path
Defensive patterns
Strategy: retry
Try / catch
if let Err(err) = channel.listen(&tx).await {
if format!("{err:#}").contains("heartbeat ACK timeout") {
tokio::time::sleep(backoff.next()).await;
continue; // zombie connection replaced; Resume recovers session
}
return Err(err);
} Prevention
- Monitor how often heartbeat timeouts occur — frequent ones mean underlying network instability
- Keep hosts from suspending the process while the gateway loop runs
- Trust the 3-miss design: do not add your own shorter timeout that tears down healthy connections
When it happens
Trigger: The hb tick fires three times with no op 11 ACK in between: a half-open TCP connection (peer gone but no RST received), severe network congestion or packet loss, or the QQ gateway stalled. missed_ack_count >= MAX_MISSED_ACKS sets ExitReason::HeartbeatTimeout at qq.rs:1502.
Common situations: Mobile or unstable links where heartbeats are lost but the socket stays 'open'; virtualization/host sleep pausing the process while the timer keeps firing on wake; asymmetric network failures behind NAT where ACKs are dropped but the connection table survives.
Understand the failure class
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- QQ WebSocket connection closed: stream ended unexpectedly
- QQ WebSocket connection closed: write failed
- QQ gateway request failed ({status}): {err}
- QQ WebSocket connection closed: invalid session (fresh auth
- QQ WebSocket connection closed: server requested reconnect (
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/687106c9aadb19bc.
Report an issue: GitHub.