redisson/redisson · error · UnsupportedOperationException

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

Error message

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

What it means

Thrown by RedissonConnection.scan(ScanOptions) on a non-cluster setup when the connection is queueing (MULTI) or pipelined. Redisson implements SCAN as a client-driven cursor loop over each master entry (see the MasterSlaveEntry iterator in the anonymous ScanCursor), so each step needs a synchronous response; deferring into a pipeline or transaction would break the iteration protocol.

Source

Thrown at redisson-spring/redisson-spring-data/redisson-spring-data-23/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java:255

        CompletableFuture<Void> ff = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
        CompletableFuture<Set<byte[]>> future = ff.thenApply(r -> {
            return futures.stream().flatMap(f -> f.getNow(new HashSet<>()).stream()).collect(Collectors.toSet());
        }).toCompletableFuture();
        return sync(new CompletableFutureWrapper<>(future));
    }

    @Override
    public Cursor<byte[]> scan(ScanOptions options) {
        return new ScanCursor<byte[]>(0, options) {

            private RedisClient client;
            private Iterator<MasterSlaveEntry> entries = executorService.getConnectionManager().getEntrySet().iterator();
            private MasterSlaveEntry entry = entries.next();
            
            @Override
            protected ScanIteration<byte[]> doScan(long cursorId, ScanOptions options) {
                if (isQueueing() || isPipelined()) {
                    throw new UnsupportedOperationException("'SSCAN' cannot be called in pipeline / transaction mode.");
                }

                if (entry == null) {
                    return null;
                }

                List<Object> args = new ArrayList<Object>();
                if (cursorId == 101010101010101010L) {
                    cursorId = 0;
                }
                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());

View on GitHub (pinned to 91188987c2)

Solutions

  1. Perform the scan before entering pipeline/transaction mode and collect keys into a list
  2. Do only per-key follow-up commands inside executePipelined
  3. Use redisson.getKeys().getKeysByPattern() or Redisson RKeys API which manages iteration without Spring Data's connection modes

Example fix

// before
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
    conn.scan(ScanOptions.scanOptions().match("user:*").build())
       .forEachRemaining(k -> conn.expire(k, 60));
    return null;
});

// after
List<byte[]> keys = new ArrayList<>();
try (Cursor<byte[]> c = redisTemplate.getConnectionFactory().getConnection()
        .scan(ScanOptions.scanOptions().match("user:*").build())) {
    c.forEachRemaining(keys::add);
}
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
    keys.forEach(k -> conn.expire(k, 60));
    return null;
});
Defensive patterns

Strategy: validation

Validate before calling

if (connection.isQueueing() || connection.isPipelined()) {
    throw new IllegalStateException("scan requires a plain connection");
}
try (Cursor<byte[]> c = connection.scan(options)) { ... }

Try / catch

try { connection.scan(options); } catch (UnsupportedOperationException e) { if (e.getMessage().contains("SSCAN")) { /* rerun on fresh non-pipelined connection */ } else throw e; }

Prevention

When it happens

Trigger: connection.scan(options) called after multi() or inside executePipelined/executeWithSession on a standard (non-cluster) RedissonConnection.

Common situations: Using RedisTemplate.keys() alternatives inside a pipeline for cache warming or TTL refresh loops; wrapping generic data-access callbacks in SessionCallback that implicitly enables queueing; copying sample code that scans keys inside a transactional callback.

Related errors


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