hyperium/hyper · error · hyper::Error

keep-alive timed out

Error message

keep-alive timed out

What it means

Thrown by the HTTP/2 keep-alive machinery (KeepAliveTimedOut, src/proto/h2/ping.rs:507 Display, wrapped at :501 via crate_error). hyper sends an HTTP/2 PING after keep_alive_interval of inactivity; if no PONG (or any frame) arrives within keep_alive_timeout, the connection is considered dead and the error is surfaced. The underlying source is crate::error::TimedOut, Kind::Http2.

Source

Thrown at src/proto/h2/ping.rs:501

    fn maybe_timeout(&mut self, cx: &mut task::Context<'_>) -> Result<(), KeepAliveTimedOut> {
        match self.state {
            KeepAliveState::PingSent => {
                if Pin::new(&mut self.sleep).poll(cx).is_pending() {
                    return Ok(());
                }
                trace!("keep-alive timeout ({:?}) reached", self.timeout);
                Err(KeepAliveTimedOut)
            }
            KeepAliveState::Init | KeepAliveState::Scheduled(..) => Ok(()),
        }
    }
}

// ===== impl KeepAliveTimedOut =====

impl KeepAliveTimedOut {
    pub(super) fn crate_error(self) -> crate::Error {
        crate::Error::new(crate::error::Kind::Http2).with(self)
    }
}

impl fmt::Display for KeepAliveTimedOut {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("keep-alive timed out")
    }
}

impl std::error::Error for KeepAliveTimedOut {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&crate::error::TimedOut)
    }
}

View on GitHub (pinned to 084473f728)

Solutions

  1. Increase keep_alive_timeout (and/or keep_alive_interval) to tolerate the peer's real RTT/processing time.
  2. If the timeout is legitimately short, retry the request on a fresh connection (the old one is dead).
  3. Verify network path: ensure H2 PING frames aren't being dropped by a middlebox/proxy.
  4. Confirm the peer (server or client) and any intermediary support H2 keep-alive PINGs.
  5. Disable keep-alive probing (http2_keep_alive_interval = None) only if an external liveness check exists.

Example fix

// before: aggressive keep-alive timeout
let client = hyper::Client::builder()
    .http2_keep_alive_interval(Some(Duration::from_secs(5)))
    .keep_alive_timeout(Duration::from_millis(500)) // too tight -> error 35
    .build_http::<hyper::Body>();

// after: tolerate slow peers and retry on timeout
let client = hyper::Client::builder()
    .http2_keep_alive_interval(Some(Duration::from_secs(15)))
    .keep_alive_timeout(Duration::from_secs(20))
    .build_http::<hyper::Body>();
Defensive patterns

Strategy: retry

Validate before calling

// Configure keep-alive to match the slowest legitimate peer RTT/processing.
use std::time::Duration;
let client = hyper::Client::builder()
    .http2_keep_alive_interval(Some(Duration::from_secs(15)))
    .http2_keep_alive_timeout(Duration::from_secs(20)) // >= worst-case RTT
    .http2_keep_alive_while_idle(true)
    .build_http::<hyper::Body>();

Try / catch

// Distinguish keep-alive timeout from other h2 errors and retry idempotent calls.
async fn fetch_retry(client: &hyper::Client<...>, req: hyper::Request<hyper::Body>) -> Result<_, hyper::Error> {
    for attempt in 0..3 {
        match client.request(req.clone()).await {
            Ok(resp) => return Ok(resp),
            Err(e) => {
                let is_timeout = e.source()
                    .and_then(|s| s.downcast_ref::<std::io::Error>())
                    .map(|io| matches!(io.kind(), std::io::ErrorKind::TimedOut))
                    .unwrap_or(false)
                    || e.to_string().contains("keep-alive timed out");
                if is_timeout && attempt < 2 {
                    tracing::warn!(attempt, "h2 keep-alive timeout; retrying on new conn");
                    continue;
                }
                return Err(e);
            }
        }
    }
    unreachable!()
}

Prevention

When it happens

Trigger: An HTTP/2 peer that stops responding (no PONG, no data/ack frames) for longer than keep_alive_timeout after hyper sent a keep-alive PING. Configured on the client via Client::builder().http2_keep_alive_interval(...).keep_alive_timeout(...) (and while_idle), and on the server analogously.

Common situations: A backend that hangs (deadlock, GC pause, overloaded event loop); a network blackhole (path MTU, firewall dropping PING frames); keep_alive_timeout set too low for a slow/high-latency peer; peer/proxy that doesn't implement H2 PING acks; mobile clients that sleep without closing the connection.

Related errors


AI-assisted analysis of hyperium/hyper@084473f728 (2026-08-06). Data as JSON: /data/errors/103337a790ef41c4.json. Report an issue: GitHub.