chroma-core/chroma · error · ValueError

Limit offset must be non-negative, got {offset}

Error message

Limit offset must be non-negative, got {offset}

What it means

Limit.from_dict rejects negative offsets with ValueError: an offset counts records to skip from the start, so any value below 0 is meaningless. The check is offset < 0, run after the int type check, so only integers can reach it.

Source

Thrown at chromadb/execution/expression/operator.py:575

    @staticmethod
    def from_dict(data: Dict[str, Any]) -> "Limit":
        """Create Limit from dictionary.

        Examples:
        - {"offset": 10} -> Limit(offset=10)
        - {"offset": 10, "limit": 20} -> Limit(offset=10, limit=20)
        - {"limit": 20} -> Limit(offset=0, limit=20)
        """
        if not isinstance(data, dict):
            raise TypeError(f"Expected dict for Limit, got {type(data).__name__}")

        offset = data.get("offset", 0)
        if not isinstance(offset, int):
            raise TypeError(
                f"Limit offset must be an integer, got {type(offset).__name__}"
            )
        if offset < 0:
            raise ValueError(f"Limit offset must be non-negative, got {offset}")

        limit = data.get("limit")
        if limit is not None:
            if not isinstance(limit, int):
                raise TypeError(
                    f"Limit limit must be an integer, got {type(limit).__name__}"
                )
            if limit <= 0:
                raise ValueError(f"Limit limit must be positive, got {limit}")

        # Check for unexpected keys
        allowed_keys = {"offset", "limit"}
        unexpected_keys = set(data.keys()) - allowed_keys
        if unexpected_keys:
            raise ValueError(f"Unexpected keys in Limit dict: {unexpected_keys}")

        return Limit(offset=offset, limit=limit)

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Clamp the computed offset: offset = max(0, (page - 1) * page_size).
  2. Treat page 0 and page 1 the same: page = max(page, 1).
  3. Validate user-supplied page/offset params at the API edge before building the Search payload.

Example fix

# before
offset = (page - 1) * page_size       # page=0 -> -10 -> ValueError
Search(limit={'offset': offset, 'limit': page_size})

# after
offset = max(0, (page - 1) * page_size)
Search(limit={'offset': offset, 'limit': page_size})
Defensive patterns

Strategy: validation

Validate before calling

def safe_offset(page: int, page_size: int) -> int:
    return max(0, (max(page, 1) - 1) * page_size)

Try / catch

try:
    Search(limit=limit_cfg)
except ValueError as e:
    if 'offset' in str(e):
        limit_cfg = {**limit_cfg, 'offset': 0}  # or return 400 to the client
    else:
        raise

Prevention

When it happens

Trigger: Limit.from_dict({'offset': -1}); page arithmetic going below zero, e.g. {'offset': (page - 1) * page_size} with page=0; cursor logic like total - fetched when fetched > total.

Common situations: 1-based page counters fed into a 0-based formula; clients sending page=0 to an API that computes offset=(page-1)*size; paging state carried between requests and underflowing on the short last page.

Related errors


AI-assisted analysis of chroma-core/chroma@aecdd12c8a (2026-08-16). Data as JSON: /api/errors/a284a5f83eb0c438. Report an issue: GitHub.