go-redis/redis · error
redis: TS.NRANGE/TS.NREVRANGE requires exactly %d aggregator
Error message
redis: TS.NRANGE/TS.NREVRANGE requires exactly %d aggregator spec(s), got %d
What it means
Returned by buildNRangeAggregationArgs (used by TS.NRANGE/TS.NREVRANGE via TSNRangeWithArgs/TSNRevRangeWithArgs) when the number of aggregator specs does not equal the number of keys. Each key must receive exactly one comma-joined aggregator spec, so len(aggregators) must equal len(keys).
Source
Thrown at timeseries_commands.go:1377
if cmd.val[i].Values != nil {
val[i].Values = make([]float64, len(cmd.val[i].Values))
copy(val[i].Values, cmd.val[i].Values)
}
}
}
return &TSNRangePivotRowSliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
// buildNRangeAggregationArgs validates and returns one aggregator spec string per key for
// TS.NRANGE / TS.NREVRANGE. The number of specs must equal the number of keys. Each spec
// lists one or more aggregators for its key and is emitted as a single comma-joined wire
// token; specs for different keys are separate wire tokens.
func buildNRangeAggregationArgs(keys []string, aggregators [][]Aggregator) ([]string, error) {
if len(aggregators) != len(keys) {
return nil, fmt.Errorf("redis: TS.NRANGE/TS.NREVRANGE requires exactly %d aggregator spec(s), got %d", len(keys), len(aggregators))
}
parts := make([]string, len(aggregators))
for i, spec := range aggregators {
if len(spec) == 0 {
return nil, fmt.Errorf("redis: empty timeseries aggregator spec at index %d", i)
}
names := make([]string, len(spec))
for j, agg := range spec {
if agg == Invalid {
return nil, fmt.Errorf("redis: invalid timeseries aggregator at index %d[%d]: Invalid (%d)", i, j, agg)
}
s := agg.String()
if s == "" {
return nil, fmt.Errorf("redis: invalid timeseries aggregator at index %d[%d]: %d", i, j, agg)
}
names[j] = s
}
parts[i] = strings.Join(names, ",")View on GitHub (pinned to 36d97525cd)
Solutions
- Pass exactly one []Aggregator per key: len(aggregators) == len(keys).
- If the same aggregator applies to all keys, build the slice programmatically: specs := make([][]redis.Aggregator, len(keys)); for i := range specs { specs[i] = []redis.Aggregator{redis.Avg} }.
- Add an assertion before the call that the two slice lengths match.
Example fix
// before
keys := []string{"ts1", "ts2"}
opts := &redis.TSNRangeOptions{Aggregators: [][]redis.Aggregator{{redis.Avg}}}
client.TSNRangeWithArgs(ctx, keys, 0, -1, opts)
// after
keys := []string{"ts1", "ts2"}
opts := &redis.TSNRangeOptions{
Aggregators: [][]redis.Aggregator{{redis.Avg}, {redis.Avg}},
}
client.TSNRangeWithArgs(ctx, keys, 0, -1, opts) Defensive patterns
Strategy: validation
Validate before calling
func validateNRangeAggregators(keys []string, aggs [][]redis.Aggregator) error {
if len(aggs) != len(keys) {
return fmt.Errorf("need %d aggregator specs for %d keys", len(keys), len(aggs))
}
return nil
} Prevention
- Derive aggregator specs from the keys slice so lengths stay in sync.
- Add an assertion len(aggregators) == len(keys) before calling TSNRangeWithArgs.
- When reusing the same aggregator for all keys, build specs in a loop keyed by keys.
When it happens
Trigger: Calling TSNRangeWithArgs or TSNRevRangeWithArgs with a keys slice of length N but an aggregators slice of length M != N (including M=0 when keys are non-empty and aggregation is requested).
Common situations: Building keys and aggregators from different data sources that got out of sync, passing one aggregator spec intending it to apply to all keys, or forgetting to add a spec per key.
Related errors
- redis: empty timeseries aggregator spec at index %d
- redis: invalid timeseries aggregator at index %d[%d]: Invali
- redis: invalid timeseries aggregator at index %d[%d]: %d
- redis: FILTER_BY_VALUE requires exactly 2 elements [min, max
- redis: invalid timeseries aggregator at index %d: Invalid (%
AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06).
Data as JSON: /data/errors/cfd9c8119f6998e4.json.
Report an issue: GitHub.