vitessio/vitess · warning

stream send error: %v

Error message

stream send error: %v

What it means

During VStreamResults, resultstreamer.Stream sends the initial fields/Gtid response over the VStreamResults gRPC stream. If the send callback fails (client disconnected, context canceled, RPC broken), it is wrapped as 'stream send error: %v'. The stream cannot continue without a live consumer, so the error propagates back to StreamResults.

Source

Thrown at go/vt/vttablet/tabletserver/vstreamer/resultstreamer.go:98

	if rotatedLog {
		rs.vse.vstreamerFlushedBinlogs.Add(1)
	}
	if err != nil {
		return err
	}

	// first call the callback with the fields
	flds, err := conn.Fields()
	if err != nil {
		return err
	}

	err = rs.send(&binlogdatapb.VStreamResultsResponse{
		Fields: flds,
		Gtid:   gtid,
	})
	if err != nil {
		return fmt.Errorf("stream send error: %v", err)
	}

	response := &binlogdatapb.VStreamResultsResponse{}
	byteCount := 0
	loggerName := fmt.Sprintf("%s (%v)", rs.vse.GetTabletInfo(), rs.tableName)
	logger := logutil.NewThrottledLogger(loggerName, throttledLoggerInterval)
	for {
		select {
		case <-rs.ctx.Done():
			return fmt.Errorf("stream ended: %v", rs.ctx.Err())
		default:
		}

		// check throttler.
		if _, ok := rs.vse.throttlerClient.ThrottleCheckOKOrWaitAppName(rs.ctx, throttlerapp.ResultStreamerName); !ok {
			logger.Infof("throttled.")
			continue
		}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Inspect the wrapped cause for the actual transport error (EOF, context canceled, connection refused).
  2. Restart the VStreamResults request from a known gtid once the consumer is healthy.
  3. Check connectivity/timeout settings between the consumer and the tablet (gRPC keepalives, LB idle timeouts).
  4. Retry with backoff — streams are resumable by design via the gtid; treat this as a transient stream failure unless the cause is deterministic.

Example fix

// before
resp, err := vstreamResults(ctx, tablet, query) // one-shot, dies on transient send error
// after
for attempt := 0; attempt < maxRetries; attempt++ {
    resp, err := vstreamResults(ctx, tablet, query)
    if err == nil || !isTransientStreamError(err) {
        break
    }
    time.Sleep(backoff(attempt))
}
Defensive patterns

Strategy: retry

Validate before calling

// before streaming, confirm the tablet is reachable and the stream target is serving
if err := tabletHealthCheck(ctx, tabletAlias); err != nil {
    return fmt.Errorf("tablet not ready for VStreamResults: %w", err)
}

Type guard

func isTransientStreamError(err error) bool {
    s := err.Error()
    return strings.Contains(s, "stream send error") &&
        (strings.Contains(s, "context canceled") || strings.Contains(s, "EOF") ||
            strings.Contains(s, "Unavailable") || strings.Contains(s, "Broken pipe"))
}

Try / catch

err := streamVStreamResults(ctx, tablet, query, handler)
if err != nil {
    if isTransientStreamError(err) {
        return retryWithBackoff(ctx, func() error { return streamVStreamResults(ctx, tablet, query, handler) })
    }
    return fmt.Errorf("VStreamResults failed permanently: %w", err)
}

Prevention

When it happens

Trigger: The VStreamResults client closes its connection or cancels its context while the tablet is streaming results; network interruption between vtctld/vtgate and the tablet mid-stream; the send callback returns a transport error on the initial Fields/Gtid response or during row sends.

Common situations: Consumer timeout or restart while streaming a large table snapshot; load balancer killing long-lived streams; tablet draining/restart during a VStreamResults call; client crash during MoveTables-style result streaming.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/e4bf5dad29ecf5d0. Report an issue: GitHub.