redis/redis-py · error · DataError

AGGREGATION requires exactly one aggregation spec per key

Error message

AGGREGATION requires exactly one aggregation spec per key (expected {numkeys}, got {len(specs)}); a spec may list multiple comma-separated aggregators.

What it means

Raised by _append_n_aggregation (TS.NRANGE / TS.NREVRANGE) when the number of aggregation specs does not equal the number of queried keys. NRANGE takes exactly one aggregation spec per key (each spec may itself be a comma-joined list of aggregators); a count mismatch is rejected client-side. The message reports expected (numkeys) vs received (len(specs)).

Solutions

  1. Provide exactly one spec per key: aggregators=['avg','max'] for two keys.
  2. For multiple aggregators on one key, comma-join them: aggregators='avg,max' for a single key.
  3. For a single-key query, a bare string is fine: aggregators='avg'.

Example fix

// before
client.ts().nrange(['k1','k2'], f, t, aggregators='avg')
// after
client.ts().nrange(['k1','k2'], f, t, aggregators=['avg','max'])
# multiple aggregators on a single key:
client.ts().nrange(['k1'], f, t, aggregators='avg,max')
Defensive patterns

Strategy: validation

Validate before calling

if aggregators is not None:
    specs = [aggregators] if isinstance(aggregators, str) else list(aggregators)
    assert len(specs) == len(keys), f'{len(specs)} specs for {len(keys)} keys'
client.ts().nrange(keys, f, t, aggregators=aggregators)

Try / catch

from redis.exceptions import DataError
try:
    client.ts().nrange(keys, f, t, aggregators=aggregators)
except DataError:
    aggregators = [aggregators] * len(keys) if isinstance(aggregators, str) else aggregators
    client.ts().nrange(keys, f, t, aggregators=aggregators)

Prevention

When it happens

Trigger: Calling nrange(['k1','k2'], ...) with aggregators='avg' (one spec for two keys), or aggregators=['avg','max','sum'] (three specs for two keys). Passing a single string when multiple keys are present.

Common situations: Assuming a single aggregator broadcasts across all keys (the old behavior pre-RedisTimeSeries #2079). Mixing up NRANGE semantics with MRANGE. Off-by-one when building the spec list.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/cc7b0aa25a99ec39. Report an issue: GitHub.

Appendix: source

Thrown at redis/commands/timeseries/commands.py:2062

        bucket_size_msec: int | None,
        numkeys: int,
    ):
        """Append the AGGREGATION clause for TS.NRANGE / TS.NREVRANGE.

        These commands take exactly one aggregation spec token per queried key;
        a single aggregator is never broadcast across keys. A spec token may hold
        several comma-separated aggregators, so the wire form is e.g.
        ``AGGREGATION avg,max sum 1000`` for two keys -- key 0 aggregated by both
        ``avg`` and ``max``, key 1 by ``sum``. Pass one spec string per key, each
        optionally comma-joined (``["avg,max", "sum"]``); a bare string is the
        spec for a single-key query. (Matches RedisTimeSeries PR #2079, which
        replaced the earlier single comma-joined / broadcast token.)
        """
        if aggregators is None:
            return
        specs = [aggregators] if isinstance(aggregators, str) else list(aggregators)
        if len(specs) != numkeys:
            raise DataError(
                "AGGREGATION requires exactly one aggregation spec per key "
                f"(expected {numkeys}, got {len(specs)}); a spec may list "
                "multiple comma-separated aggregators."
            )
        params.extend(["AGGREGATION", *specs, bucket_size_msec])

    @staticmethod
    def _append_chunk_size(params: list[EncodableT], chunk_size: int | None):
        """Append CHUNK_SIZE property to params."""
        if chunk_size is not None:
            params.extend(["CHUNK_SIZE", chunk_size])

    @staticmethod
    def _append_duplicate_policy(
        params: list[EncodableT], duplicate_policy: str | None
    ):
        """Append DUPLICATE_POLICY property to params."""
        if duplicate_policy is not None:

View on GitHub (pinned to 6a6b581b48)