thanos-io/thanos · error

index header PostingsOffset

Error message

index header PostingsOffset

What it means

When the requested posting list is not in cache, bucketIndexReader asks the block's index header for the byte offset of the postings table for a key. Wrapping an error from that lookup means the index header could not be consulted (or the offset could not be resolved) for postingPtr. This typically indicates the index header itself failed to load or the postings entry referenced does not exist in the block's header.

Solutions

  1. Re-sync the store-gateway bucket index so block metadata matches what is actually in object storage
  2. Check the underlying wrapped error: if the block is missing/deleted, drop it from local view and retry the query
  3. Re-upload or restore the block's index (meta.json/index) if the header is corrupted in the bucket
  4. Ensure block deletion uses proper two-step (deletion-mark) flow so readers stop using the block before removal
Defensive patterns

Strategy: retry

Validate before calling

if _, ok := r.block.indexHeader.GetPostingsOffset(name, value); !ok {
	// postings offset absent: resync block metadata or skip this block
}

Type guard

func blockServesPosting(ih index.Header, name, value string) bool { _, ok := ih.GetPostingsOffset(name, value); return ok }

Try / catch

output, closeFns, err := reader.Postings(ctx, tenant, postingGroups...)
if err != nil && strings.Contains(err.Error(), "index header PostingsOffset") {
	if rerr := storeGateway.ResyncBlocks(ctx); rerr == nil {
		output, closeFns, err = reader.Postings(ctx, tenant, postingGroups...) // retry once after resync
	}
}

Prevention

When it happens

Trigger: Calling Postings() with a label name/value whose offset lookup in the loaded index header fails: index header not yet loaded/refreshed for the block, block was deleted or replaced in object storage mid-query, or a corrupted/unsupported index header binary.

Common situations: Store-gateway serving a block whose meta was removed from the bucket between listing and querying; race between block removal (compaction) and in-flight queries; corrupted index header file in object storage after a failed upload.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at pkg/store/bucket.go:3166

			l, closer, err := r.decodeCachedPostings(b)
			if err != nil {
				return nil, closeFns, errors.Wrap(err, "decode postings")
			}
			output[ix] = l
			closeFns = append(closeFns, closer...)
			continue
		}

		// Cache miss; save pointer for actual posting in index stored in object store.
		ptr, err := r.block.indexHeaderReader.PostingsOffset(key.Name, key.Value)
		if err == indexheader.NotFoundRangeErr {
			// This block does not have any posting for given key.
			output[ix] = index.EmptyPostings()
			continue
		}

		if err != nil {
			return nil, closeFns, errors.Wrap(err, "index header PostingsOffset")
		}

		r.stats.postingsToFetch++
		ptrs = append(ptrs, postingPtr{ptr: ptr, keyID: ix})
	}

	sort.Slice(ptrs, func(i, j int) bool {
		return ptrs[i].ptr.Start < ptrs[j].ptr.Start
	})

	// TODO(bwplotka): Asses how large in worst case scenario this can be. (e.g fetch for AllPostingsKeys)
	// Consider sub split if too big.
	parts := r.block.partitioner.Partition(len(ptrs), func(i int) (start, end uint64) {
		return uint64(ptrs[i].ptr.Start), uint64(ptrs[i].ptr.End)
	})

	size = 0
	for _, part := range parts {

View on GitHub (pinned to 35b8b99117)