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
- Raise or remove the per-response .timeout(dur) if the body legitimately streams slowly.
- Stream the body with explicit chunk handling and backpressure instead of buffering it all under one deadline.
- Diagnose the upstream: it may be stalled, rate-limited, or waiting on a slow dependency.
- 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
- Only enable .timeout(dur) on the response when you expect a bounded, fast body.
- Stream large/slow bodies chunk-by-chunk with their own pacing instead of one hard deadline.
- Keep the request-level timeout and the body-read timeout distinct and sensibly sized.
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.