actix/actix-web · error · io::Error

Response Payload IO timed out

Error message

Response Payload IO timed out

What it means

PayloadError::Io(io::Error(TimedOut, "Response Payload IO timed out")) (mod.rs:37-40) is produced by ResponseTimeout::poll_timeout in awc when the deadline set via ClientResponse::timeout(dur) elapses while the response body is still being streamed. The timeout is disabled by default (ResponseTimeout::default() is Disabled) and only armed by an explicit .timeout() call or a request-level timeout whose Sleep is reused via _timeout. It wraps the body poll with a race against a tokio Sleep.

Source

Thrown at awc/src/responses/mod.rs:37

///
/// See [`ClientResponse::_timeout`] for reason.
pub(crate) enum ResponseTimeout {
    Disabled(Option<Pin<Box<Sleep>>>),
    Enabled(Pin<Box<Sleep>>),
}

impl Default for ResponseTimeout {
    fn default() -> Self {
        Self::Disabled(None)
    }
}

impl ResponseTimeout {
    fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
        match *self {
            Self::Enabled(ref mut timeout) => {
                if timeout.as_mut().poll(cx).is_ready() {
                    Err(PayloadError::Io(io::Error::new(
                        io::ErrorKind::TimedOut,
                        "Response Payload IO timed out",
                    )))
                } else {
                    Ok(())
                }
            }
            Self::Disabled(_) => Ok(()),
        }
    }
}

View on GitHub (pinned to 937960ca67)

Solutions

  1. Raise or remove the per-response .timeout(dur) if the body legitimately streams slowly.
  2. Stream the body with explicit chunk handling and backpressure instead of buffering it all under one deadline.
  3. Diagnose the upstream: it may be stalled, rate-limited, or waiting on a slow dependency.
  4. Set the request-level timeout (ClientRequest::timeout) appropriately distinct from the body-read timeout.

Example fix

// before: tight body timeout on a slow stream
let body = res.timeout(Duration::from_millis(50)).body().await?;

// after: raise the deadline or stream incrementally
let body = res.timeout(Duration::from_secs(30)).body().await?;
// or stream without a single hard deadline:
// while let Some(chunk) = res.next().await { ... }
Defensive patterns

Strategy: try-catch

Validate before calling

// Size the timeout from the expected body size and throughput.
fn body_timeout(bytes: usize, bytes_per_sec: usize) -> std::time::Duration {
    std::time::Duration::from_secs((bytes as f64 / bytes_per_sec as f64).ceil() as u64 + 5)
}

Try / catch

// Distinguish the awc payload timeout from other IO errors.
use actix_http::error::PayloadError;
match res.body().await {
    Err(PayloadError::Io(e)) if e.kind() == std::io::ErrorKind::TimedOut => {
        // response body deadline elapsed; retry or fall back
    }
    Err(e) => { /* other payload errors */ }
    Ok(body) => { /* ... */ }
}

Prevention

When it happens

Trigger: An awc ClientResponse body read (e.g. .body(), .json(), or manual stream polling) is configured with a timeout and the server delivers body bytes slower than the deadline. poll_timeout (mod.rs:33-47) fires and returns the IO timed-out error.

Common situations: Slow upstream server, streaming endpoints with infrequent chunks, a timeout set too low for large payloads, or a server that holds the connection open without sending data.

Related errors


AI-assisted analysis of actix/actix-web@937960ca67 (2026-08-06). Data as JSON: /data/errors/a28553555af33893.json. Report an issue: GitHub.