thanos-io/thanos · error

no tsdb statistics

Error message

no tsdb statistics

What it means

The gRPC streaming handler Send() in pkg/status/status.go:78 processes a TSDBStatisticsResponse and requires a non-nil Statistics payload. If a response arrives with neither a warning string nor a Statistics map, the server aborts the stream with 'no tsdb statistics'. This guards the aggregation logic that merges per-tenant TSDB stats from receiving an empty protobuf message.

Solutions

  1. Upgrade all Thanos components so peers send a populated TSDBStatisticsResponse (or a Warning instead of an empty message)
  2. Check the responding component's implementation/mock: it must set either Warning or Statistics before calling Send
  3. Capture gRPC client/server logs to identify which peer produced the empty response
  4. If the error is transient for one unreachable store, treat it as a warning for that store and continue streaming from others

Example fix

// before (peer handler)
res := &statuspb.TSDBStatisticsResponse{}
if err := stream.Send(res); err != nil { return err }
// after
res := &statuspb.TSDBStatisticsResponse{Statistics: stats}
if len(stats.Statistics) == 0 {
    res = &statuspb.TSDBStatisticsResponse{Warning: "no statistics available"}
}
if err := stream.Send(res); err != nil { return err }
Defensive patterns

Strategy: type-guard

Validate before calling

if res.GetWarning() == "" && res.GetStatistics() == nil {
    return fmt.Errorf("peer %s returned empty TSDB statistics response", peerAddr)
}

Type guard

func hasStatistics(res *statuspb.TSDBStatisticsResponse) bool {
    return res.GetWarning() != "" || res.GetStatistics() != nil
}

Try / catch

if err := srv.Send(res); err != nil {
    if err.Error() == "no tsdb statistics" {
        logger.Warn("skipping peer with empty statistics response", "peer", peerAddr)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling Status.TSDBStatistics when the remote side returns a TSDBStatisticsResponse that has an empty Warning field and a nil Statistics field — i.e. a zero-value or partially populated response message from the peer.

Common situations: Version skew between Thanos components where an older peer does not populate Statistics; a gRPC server implementation (mock, proxy, or third-party) that returns an empty response; proto serialization where the Statistics oneof/field was dropped by an intermediary.

Understand the failure class

Background: "empty response", "returned no data", "empty embeddings": what HTTP 200-with-empty-body errors mean across libraries — this error's family across 36 libraries.

Related errors


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

Appendix: source

Thrown at pkg/status/status.go:88

	statuspb.Status_TSDBStatisticsServer
	ctx context.Context

	mtx            sync.Mutex
	warnings       annotations.Annotations
	tsdbStatistics map[string][]*statuspb.TSDBStatisticsEntry
}

func (srv *tsdbStatisticsServer) Send(res *statuspb.TSDBStatisticsResponse) error {
	if res.GetWarning() != "" {
		srv.mtx.Lock()
		defer srv.mtx.Unlock()
		srv.warnings.Add(errors.New(res.GetWarning()))
		return nil
	}

	stats := res.GetStatistics()
	if stats == nil {
		return errors.New("no tsdb statistics")
	}

	srv.mtx.Lock()
	defer srv.mtx.Unlock()
	for tenant, tenantStats := range stats.Statistics {
		if _, found := srv.tsdbStatistics[tenant]; !found {
			srv.tsdbStatistics[tenant] = nil
		}
		srv.tsdbStatistics[tenant] = append(srv.tsdbStatistics[tenant], tenantStats)
	}

	return nil
}

func (srv *tsdbStatisticsServer) Context() context.Context {
	return srv.ctx
}

View on GitHub (pinned to 35b8b99117)