thanos-io/thanos · error

channel closed before a value received

Error message

channel closed before a value received

What it means

querier.Select spawns a goroutine that computes the series set and sends it exactly once on the promise channel. Next() on the lazy series set receives from this channel; if it is closed before a value is delivered, the implementation returns this error — a safety net against a broken/garbage-collected select goroutine.

Solutions

  1. Look for a panic/error higher in the logs from the select goroutine
  2. Check querier lifetime: ensure the query is not cancelled/torn down while Next is pending
  3. Upgrade Thanos — fixed versions send an error series set instead of closing the channel
  4. File an issue with stack trace if reproducible
Defensive patterns

Strategy: try-catch

Try / catch

if err := ss.Err(); err != nil {
    if strings.Contains(err.Error(), "channel closed before a value received") {
        // internal select-goroutine failure; surface as 500 with logs
        return fmt.Errorf("query engine failed internally: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: The promise channel is closed without a value, e.g. the select goroutine panics/returns before sending, or internal lifecycle mishandling of the lazy series set.

Common situations: Seen during Thanos engine bugs, panics inside the select goroutine, or when the querier is torn down mid-select; not a user-facing misconfiguration.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/48f99765367e37d9. Report an issue: GitHub.

Appendix: source

Thrown at pkg/query/querier.go:351

		set, stats, err := q.selectFn(ctx, hints, ms...)
		if err != nil {
			promise <- storage.ErrSeriesSet(err)
			return
		}
		q.seriesStatsReporter(stats)

		promise <- set
	}()

	return &lazySeriesSet{create: func() (storage.SeriesSet, bool) {
		defer cancel()
		defer span.Finish()

		// Only gets called once, for the first Next() call of the series set.
		set, ok := <-promise
		if !ok {
			return storage.ErrSeriesSet(errors.New("channel closed before a value received")), false
		}
		return set, set.Next()
	}}
}

const SeriesHashLabelName = "__cf_series_hash__"

func (q *querier) selectFn(ctx context.Context, hints *storage.SelectHints, ms ...*labels.Matcher) (storage.SeriesSet, storepb.SeriesStatsCounter, error) {
	sms, err := storepb.PromMatchersToMatchers(ms...)
	if err != nil {
		return nil, storepb.SeriesStatsCounter{}, errors.Wrap(err, "convert matchers")
	}

	aggrs := aggrsFromFunc(hints.Func)
	maxResolutionMillis := maxResolutionFromSelectHints(q.maxResolutionMillis, hints.Range, hints.Func)

	// TODO(bwplotka): Pass it using the SeriesRequest instead of relying on context.
	ctx = context.WithValue(ctx, store.StoreMatcherKey, q.storeDebugMatchers)

View on GitHub (pinned to 35b8b99117)