cloudflare/cloudflared · warning

timeout waiting for second stream to finish

Error message

timeout waiting for second stream to finish

What it means

The stream `wait` helper in cloudflared's bidirectional pipe waits up to maxWaitForSecondStream for the second stream (the opposite direction) to appear or for doneChan to close. If the timer fires first, it means only one side of the proxied conversation finished/started and the paired stream never arrived within the deadline, so PipeBidirectional is aborted with this error.

Source

Thrown at stream/stream.go:74

	}
}

func (s *bidirectionalStreamStatus) markUniStreamDone() {
	atomic.StoreUint32(&s.anyDone, 1)
	s.doneChan <- struct{}{}
}

func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) error {
	<-s.doneChan

	// Only wait for second stream to finish if maxWait is greater than zero
	if maxWaitForSecondStream > 0 {
		timer := time.NewTimer(maxWaitForSecondStream)
		defer timer.Stop()

		select {
		case <-timer.C:
			return fmt.Errorf("timeout waiting for second stream to finish")
		case <-s.doneChan:
			return nil
		}
	}

	return nil
}
func (s *bidirectionalStreamStatus) isAnyDone() bool {
	return atomic.LoadUint32(&s.anyDone) > 0
}

// 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

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Check that cloudflared and Cloudflare edge versions are current; older mixed versions can frame streams differently.
  2. Verify the origin application actually uses both directions; a single-stream application design will inherently hit this wait.
  3. Tune maxWaitForSecondStream upward if legitimate slow second streams are being cut off.
  4. Investigate the client for half-open/idle connections and enable TCP keepalives to reap them.
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling: ensure a token is present and CSR is valid
token, ok := os.LookupEnv("TUNNEL_TOKEN")
if !ok || token == "" {
	return errors.New("missing Cloudflare token for cert signing")
}
if _, err := x509.ParseCertificateRequest(csr); err != nil {
	return fmt.Errorf("invalid CSR: %w", err)
}

Try / catch

cert, err := sshgen.SignCert(ctx, client, hostnames, csr)
if err != nil {
	var status int
	if _, scanErr := fmt.Sscanf(err.Error(), "%d:", &status); scanErr == nil && (status == 429 || status >= 500) {
		// transient: retry with backoff
	}
	return err
}

Prevention

When it happens

Trigger: PipeBidirectional is called for a connection (e.g. TCP-over-WebSocket/QUIC) where only one data stream is ever opened and the peer never initiates the second stream before maxWaitForSecondStream elapses; protocol mismatch where one side expects one stream and the other opens two.

Common situations: Version mismatches between cloudflared and edge on connection framing; half-open TCP connections where a client connects, sends nothing, and idles; protocols that keep a single long-lived stream (e.g. long-polling apps) tripping the second-stream timeout.

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/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/95444f657aabb31b. Report an issue: GitHub.