cloudflare/cloudflared · error
unable to wait for both streams while proxying
Error message
unable to wait for both streams while proxying
What it means
PipeBidirectional pumps data between two connections with one goroutine per direction and waits for both to finish. This error is returned when the second stream does not complete within maxWaitForSecondStream after the first one ended, i.e. proxying stalled with one direction still open.
Source
Thrown at stream/stream.go:103
// Pipe copies copy data to & from provided io.ReadWriters.
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
}
// PipeBidirectional copies data to two unidirectional streams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the
// Bidirectional Stream.
// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,
// the method will return immediately with a timeout error. It is however, the responsibility of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
status := newBiStreamStatus()
go unidirectionalStream(downstream, upstream, "upstream->downstream", status, log)
go unidirectionalStream(upstream, downstream, "downstream->upstream", status, log)
if err := status.wait(maxWaitForSecondStream); err != nil {
return errors.Wrap(err, "unable to wait for both streams while proxying")
}
return nil
}
func unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) {
defer func() {
// The bidirectional streaming spawns 2 goroutines to stream each direction.
// If any ends, the callstack returns, meaning the Tunnel request/stream (depending on http2 vs quic) will
// close. In such case, if the other direction did not stop (due to application level stopping, e.g., if a
// server/origin listens forever until closure), it may read/write from the underlying ReadWriter (backed by
// the Edge<->cloudflared transport) in an unexpected state.
// Because of this, we set this recover() logic.
if err := recover(); err != nil {
if status.isAnyDone() {
// We handle such unexpected errors only when we detect that one side of the streaming is done.
log.Debug().Msgf("recovered from panic in stream.Pipe for %s, error %s, %s", dir, err, debug.Stack())
} else {View on GitHub (pinned to 2253eeeb25)
Solutions
- Increase maxWaitForSecondStream if legitimate long-lived half-open streams are expected
- Verify both peers properly close their sockets when done (check origin server keep-alive/close behavior)
- Check for hung reads caused by a dead peer and add application-level timeouts/keepalives
- Inspect the wrapped error to identify which direction stalled
Example fix
// before
if err := status.wait(maxWaitForSecondStream); err != nil {
return errors.Wrap(err, "unable to wait for both streams while proxying")
}
// after
const maxWaitForSecondStream = 5 * time.Minute // was too short for idle SSH sessions
if err := status.wait(maxWaitForSecondStream); err != nil {
return errors.Wrap(err, "unable to wait for both streams while proxying")
} Defensive patterns
Strategy: try-catch
Try / catch
if err := PipeBidirectional(down, up, log); err != nil {
var wrapped *errors.Wrap // cloudflared errors package
if strings.Contains(err.Error(), "unable to wait for both streams") {
log.Warn().Err(err).Msg("stream stalled; closing connection")
}
_ = wrapped
} Prevention
- Ensure peers close sockets promptly at end of session
- Enable TCP keepalives to detect dead peers
- Size maxWaitForSecondStream to your workload (long idle streams need longer waits)
When it happens
Trigger: unidirectionalStream in one direction returns (usually on read error or EOF) while the other direction blocks longer than maxWaitForSecondStream — e.g. the remote keeps the connection open without sending data.
Common situations: Half-closed connections where the origin never closes its write side; dead peers not answering FINs; long-lived idle streams (e.g. SSH, websocket) exceeding the wait budget; network partitions.
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
- Unable to reach the origin service. The service may be down
- Error writing response header
- quick tunnel provisioning failed
- funnel not found
- internal error: unsupported connection type
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/0300addc6bbcc6f4.
Report an issue: GitHub.