thanos-io/thanos · error
expand
Error message
expand
What it means
After mergeFetchedPostings builds the merged postings set, ExpandPostingsWithContext materializes it into a concrete list of posting IDs; any failure (including context cancellation mid-expansion) is wrapped as "expand". This is Thanos' lazy-postings expansion step that avoids materializing huge posting lists unless needed.
Solutions
- Check if the wrapped error is context cancellation/deadline exceeded and raise the query timeout.
- Narrow the label matchers to reduce the size of the postings set.
- Use time-based partitioning (query smaller time ranges) to shrink matching series.
- Increase store-gateway resources/limits if expansion routinely fails on large blocks.
Defensive patterns
Strategy: validation
Validate before calling
// keep posting expansion small before querying
if len(matchers) == 0 || allMatchersTooBroad(matchers) {
return errors.New("matchers too broad, narrow the selector")
} Try / catch
if err != nil && strings.Contains(err.Error(), "expand") {
if errors.Is(errors.Unwrap(err), context.DeadlineExceeded) {
// increase timeout or narrow query
}
} Prevention
- Avoid extremely broad matchers on high-cardinality labels
- Use --query.timeout appropriate for data size
- Enable downsampling so fewer series match over long ranges
When it happens
Trigger: A Series query whose merged postings are very large, causing ExpandPostingsWithContext to run long enough that the request context is cancelled, or an internal expansion failure.
Common situations: Broad matchers like {job="x"} over huge blocks expanding millions of postings; Prometheus query timeouts killing the gRPC context mid-expansion; store-gateway memory pressure.
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 thanos-io/thanos@35b8b99117 (2026-09-07).
Data as JSON: /api/errors/e8ea113054ce78d5.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/store/lazy_postings.go:316
keys, lazyMatchers := keysToFetchFromPostingGroups(postingGroups)
fetchedPostings, closeFns, err := r.fetchPostings(ctx, keys, bytesLimiter, tenant)
defer func() {
for _, closeFn := range closeFns {
closeFn()
}
}()
if err != nil {
return nil, nil, errors.Wrap(err, "get postings")
}
result := mergeFetchedPostings(ctx, fetchedPostings, postingGroups)
if err := ctx.Err(); err != nil {
return nil, nil, err
}
ps, err := ExpandPostingsWithContext(ctx, result)
r.postings = ps
if err != nil {
return nil, nil, errors.Wrap(err, "expand")
}
return ps, lazyMatchers, nil
}
func mergeFetchedPostings(ctx context.Context, fetchedPostings []index.Postings, postingGroups []*postingGroup) index.Postings {
// Get "add" and "remove" postings from groups. We iterate over postingGroups and their keys
// again, and this is exactly the same order as before (when building the groups), so we can simply
// use one incrementing index to fetch postings from returned slice.
postingIndex := 0
var groupAdds, groupRemovals []index.Postings
for _, g := range postingGroups {
if g.lazy {
continue
}
// We cannot add empty set to groupAdds, since they are intersected.
if len(g.addKeys) > 0 {
toMerge := make([]index.Postings, 0, len(g.addKeys))View on GitHub (pinned to 35b8b99117)