tokio-rs/tokio · error · io::Error

failed to write frame to transport

Error message

failed to write frame to transport

What it means

Runtime error from the `Sink` `poll_flush` of `Framed`/`FramedWrite` (framed_impl.rs). While the output buffer is non-empty, `poll_write_buf` returned `Ok(0)` — the underlying writer accepted zero bytes. Tokio treats this as a `WriteZero` failure: the transport cannot accept the frame.

Source

Thrown at tokio-util/src/codec/framed_impl.rs:290

        pinned
            .codec
            .encode(item, &mut pinned.state.borrow_mut().buffer)?;
        Ok(())
    }

    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        use crate::util::poll_write_buf;
        trace!("flushing framed transport");
        let mut pinned = self.project();

        while !pinned.state.borrow_mut().buffer.is_empty() {
            let WriteFrame { buffer, .. } = pinned.state.borrow_mut();
            trace!(remaining = buffer.len(), "writing;");

            let n = ready!(poll_write_buf(pinned.inner.as_mut(), cx, buffer))?;

            if n == 0 {
                return Poll::Ready(Err(io::Error::new(
                    io::ErrorKind::WriteZero,
                    "failed to \
                     write frame to transport",
                )
                .into()));
            }
        }

        // Try flushing the underlying IO
        ready!(pinned.inner.poll_flush(cx))?;

        trace!("framed transport flushed");
        Poll::Ready(Ok(()))
    }

    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        ready!(self.as_mut().poll_flush(cx))?;
        ready!(self.project().inner.poll_shutdown(cx))?;

View on GitHub (pinned to 625954f365)

Solutions

  1. Treat the error as terminal for that connection: drop it and reconnect.
  2. Confirm you are not writing after shutting down the transport.
  3. Inspect `io::Error::kind()` (`WriteZero`) to distinguish a dead transport from transient backpressure.
  4. For custom `AsyncWrite` impls, never return `Ok(0)` unless genuinely EOF — return `Poll::Pending` instead.

Example fix

// before: ignoring sink errors
framed.send(item).await.unwrap();

// after: treat flush failure as a closed transport
if let Err(e) = framed.send(item).await {
    if e.kind() == io::ErrorKind::WriteZero {
        // transport is closed: reconnect or shut down this peer
        reconnect().await;
    } else {
        return Err(e.into());
    }
}
Defensive patterns

Strategy: try-catch

Type guard

fn is_transport_dead(e: &io::Error) -> bool {
    matches!(e.kind(), io::ErrorKind::WriteZero | io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset)
}

Try / catch

if let Err(e) = framed.send(item).await {
    if is_transport_dead(&e) { reconnect().await; continue; }
    return Err(e.into());
}

Prevention

When it happens

Trigger: The underlying `AsyncWrite` returned `0` from `write` without an error during a `Framed` sink flush — e.g. writing to a half-closed socket, a closed pipe, or a TLS session that has shut down the write side.

Common situations: Peer closed the connection while you were still sending (broken pipe / connection reset reported as a zero write); writing after `poll_close`/`poll_shutdown`; half-open TCP socket; a custom `AsyncWrite` that returns 0 spuriously.

Related errors


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/9a4439f261abf3b9. Report an issue: GitHub.