{"record":{"id":"e8ea113054ce78d5","repo":"thanos-io/thanos","slug":"expand","errorCode":null,"errorMessage":"expand","messagePattern":"expand","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/store/lazy_postings.go","lineNumber":316,"sourceCode":"\tkeys, lazyMatchers := keysToFetchFromPostingGroups(postingGroups)\n\tfetchedPostings, closeFns, err := r.fetchPostings(ctx, keys, bytesLimiter, tenant)\n\tdefer func() {\n\t\tfor _, closeFn := range closeFns {\n\t\t\tcloseFn()\n\t\t}\n\t}()\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"get postings\")\n\t}\n\n\tresult := mergeFetchedPostings(ctx, fetchedPostings, postingGroups)\n\tif err := ctx.Err(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tps, err := ExpandPostingsWithContext(ctx, result)\n\tr.postings = ps\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"expand\")\n\t}\n\treturn ps, lazyMatchers, nil\n}\n\nfunc mergeFetchedPostings(ctx context.Context, fetchedPostings []index.Postings, postingGroups []*postingGroup) index.Postings {\n\t// Get \"add\" and \"remove\" postings from groups. We iterate over postingGroups and their keys\n\t// again, and this is exactly the same order as before (when building the groups), so we can simply\n\t// use one incrementing index to fetch postings from returned slice.\n\tpostingIndex := 0\n\n\tvar groupAdds, groupRemovals []index.Postings\n\tfor _, g := range postingGroups {\n\t\tif g.lazy {\n\t\t\tcontinue\n\t\t}\n\t\t// We cannot add empty set to groupAdds, since they are intersected.\n\t\tif len(g.addKeys) > 0 {\n\t\t\ttoMerge := make([]index.Postings, 0, len(g.addKeys))","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/thanos-io/thanos/blob/35b8b991177def87ed52dcf10f9b6d87f07282c8/pkg/store/lazy_postings.go#L298-L334","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// keep posting expansion small before querying\nif len(matchers) == 0 || allMatchersTooBroad(matchers) {\n  return errors.New(\"matchers too broad, narrow the selector\")\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"expand\") {\n  if errors.Is(errors.Unwrap(err), context.DeadlineExceeded) {\n    // increase timeout or narrow query\n  }\n}","preventionTips":["Avoid extremely broad matchers on high-cardinality labels","Use --query.timeout appropriate for data size","Enable downsampling so fewer series match over long ranges"],"tags":["go","index","context-cancelled"],"backgroundTag":"request-timeout","analyzedSha":"35b8b991177def87ed52dcf10f9b6d87f07282c8","analyzedAt":"2026-09-07T01:49:59.689Z","contentChangedAt":"2026-09-07T01:49:59.689Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}