cloudflare/quiche · warning · io::Error

TimedOut

TimedOut

Error message

connection {scid:?} timed out

What it means

When tokio-quiche's connection router accepts a new incoming connection (on_incoming), a connection that has already fully closed (never completed its handshake) is reported as TimedOut with the connection's SCID. A closed-before-handshake connection is treated as a handshake timeout.

Solutions

  1. Inspect server logs for the SCID to correlate with the offending client
  2. Check client QUIC version and initial packet validity
  3. If this happens en masse, review timeouts (max_idle_timeout) and any middleboxes dropping handshake packets
  4. Treat occasional occurrences as normal Internet noise
Defensive patterns

Strategy: try-catch

Try / catch

match router.handle_initials(...).await {
    Err(e) if e.kind() == io::ErrorKind::TimedOut => {
        log::warn!("handshake closed before completing: {e}"); // expected client churn
    }
    other => other?,
}

Prevention

When it happens

Trigger: An incoming QUIC initial that gets closed before the handshake completes — conn.is_closed() is true at on_incoming time, called from handle_initials.

Common situations: Clients sending malformed or stale initials; idle/dead clients probing the server; version-negotiation failures; load balancer health checks that open and abort connections.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of cloudflare/quiche@9f96daa2c2 (2026-09-08). Data as JSON: /api/errors/871b9e2753fa1b10. Report an issue: GitHub.

Appendix: source

Thrown at tokio-quiche/src/quic/router/connector.rs:191

            self.timeout_queue.remove(&key);
        }

        let scid = conn.source_id();
        if conn.is_established() {
            log::debug!("QUIC connection established"; "scid" => ?scid);

            Ok(Some(NewConnection {
                conn,
                pending_cid: None,
                initial_pkt: None,
                cid_generator: None,
                handshake_start_time,
            }))
        } else if conn.is_closed() {
            let scid = conn.source_id();
            log::error!("QUIC connection closed on_incoming"; "scid" => ?scid);

            Err(io::Error::new(
                io::ErrorKind::TimedOut,
                format!("connection {scid:?} timed out"),
            ))
        } else {
            self.set_connection_to_pending(conn).map(|()| None)
        }
    }

    /// [`ClientConnector::on_timeout`] runs [`quiche::Connection::on_timeout`]
    /// for a pending connection. If the connection is closed, this sends an
    /// error upstream.
    fn on_timeout(&mut self, scid: ConnectionId<'static>) -> io::Result<()> {
        log::debug!("connection timedout"; "scid" => ?scid);

        let Some(mut pending) =
            self.connection.take_if_pending_and_id_matches(&scid)
        else {
            log::debug!("timedout connection missing from pending map"; "scid" => ?scid);

View on GitHub (pinned to 9f96daa2c2)