redisson/redisson · error · UnsupportedOperationException

'ZSCAN' cannot be called in pipeline / transaction mode.

Error message

'ZSCAN' cannot be called in pipeline / transaction mode.

What it means

Redisson's Spring Data Redis adapter (redisson-spring-data-18) throws UnsupportedOperationException from zScan(byte[], ScanOptions) when ZSCAN cursor iteration is attempted while the connection is queueing (MULTI/transaction) or pipelined. ZSCAN needs interactive round trips per cursor page, which a deferred pipeline/transaction buffer cannot provide.

Source

Thrown at redisson-spring/redisson-spring-data/redisson-spring-data-18/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java:1297

        if (aggregate != null) {
            args.add("AGGREGATE");
            args.add(aggregate.name());
        }
        return write(destKey, StringCodec.INSTANCE, ZINTERSTORE, args.toArray());
    }

    private static final RedisCommand<ListScanResult<Object>> ZSCAN = new RedisCommand<>("ZSCAN", new ListMultiDecoder2(new ListScanResultReplayDecoder(), new ScoredSortedListReplayDecoder()));
    
    @Override
    public Cursor<Tuple> zScan(byte[] key, ScanOptions options) {
        return new KeyBoundCursor<Tuple>(key, 0, options) {

            private RedisClient client;

            @Override
            protected ScanIteration<Tuple> doScan(byte[] key, long cursorId, ScanOptions options) {
                if (isQueueing() || isPipelined()) {
                    throw new UnsupportedOperationException("'ZSCAN' cannot be called in pipeline / transaction mode.");
                }

                List<Object> args = new ArrayList<Object>();
                args.add(key);
                args.add(Long.toUnsignedString(cursorId));
                if (options.getPattern() != null) {
                    args.add("MATCH");
                    args.add(options.getPattern());
                }
                if (options.getCount() != null) {
                    args.add("COUNT");
                    args.add(options.getCount());
                }
                
                RFuture<ListScanResult<Tuple>> f = executorService.readAsync(client, key, ByteArrayCodec.INSTANCE, ZSCAN, args.toArray());
                ListScanResult<Tuple> res = syncFuture(f);
                client = res.getRedisClient();
                return new ScanIteration<Tuple>(Long.parseUnsignedLong(res.getPos()), res.getValues());

View on GitHub (pinned to 91188987c2)

Solutions

  1. Run zScan on a non-pipelined, non-transactional connection (separate RedisTemplate call outside the pipeline block)
  2. Use ZRANGE/ZRANGEBYSCORE with explicit offsets inside pipelines when the set is bounded and small
  3. Scan first into a collection, then pipeline the per-member follow-up operations

Example fix

// before
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
    conn.zScan(key.getBytes(), ScanOptions.NONE);
    return null;
});

// after
try (Cursor<Tuple> c = redisTemplate.opsForZSet().scan(key, ScanOptions.NONE)) {
    List<Object> tuples = new ArrayList<>();
    c.forEachRemaining(tuples::add);
}
// then pipeline follow-up ops on the collected tuples
Defensive patterns

Strategy: validation

Validate before calling

// Run zScan via the template on a non-pipelined path
try (Cursor<Tuple> c = redisTemplate.opsForZSet().scan(zsetKey, ScanOptions.scanOptions().count(500).build())) {
    c.forEachRemaining(t -> handle(t.getValue()));
} // ensure no executePipelined wrapper encloses this call

Try / catch

catch (UnsupportedOperationException e) {
    if (e.getMessage() != null && e.getMessage().contains("ZSCAN")) {
        // fall back to ZRANGE pagination outside the pipeline
    } else throw e;
}

Prevention

When it happens

Trigger: Calling zScan(...) (directly or via ZSetOperations.scan / RedisTemplate.boundZSetOps(key).scan()) inside executePipelined(...) or inside a multi()/exec() session on the same connection.

Common situations: Batch-processing sorted sets inside a wrapped executePipelined block for throughput; porting Jedis-based code that pipelined ZSCAN; leaderboard pagination code moved inside a Redis transaction; version upgrade exposing the previously untested path.

Related errors


AI-assisted analysis of redisson/redisson@91188987c2 (2026-08-14). Data as JSON: /api/errors/bb29adfe6775934b. Report an issue: GitHub.