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

scan() returns a lazy ScanCursor whose doScan() issues SCAN and needs the reply (next cursor id) before the next call. While the connection is queueing (MULTI) or pipelined, replies are deferred until EXEC, so the cursor cannot operate and UnsupportedOperationException is thrown. The message says 'SSCAN' but this is the keyspace SCAN path — a copy-paste in the message text.

Source

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

        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. Run scan() on a plain, non-pipelined connection
  2. Use RedisTemplate.scan(options), which binds its own connection
  3. If inside a pipeline, fall back to KEYS pattern for small keyspaces

Example fix

// before
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
    Cursor<byte[]> c = conn.scan(options);
    c.forEachRemaining(keys::add);
    return null;
});

// after
try (Cursor<byte[]> c = redisTemplate.scan(options)) {
    c.forEachRemaining(k -> keys.add(k));
}
Defensive patterns

Strategy: validation

Validate before calling

if (connection.isPipelined() || connection.isQueueing()) {
    throw new IllegalStateException("scan() cannot run in pipeline/tx");
}
Cursor<byte[]> cursor = connection.scan(options);

Type guard

boolean canScan(RedisConnection conn) {
    return !conn.isPipelined() && !conn.isQueueing();
}

Try / catch

try (Cursor<byte[]> c = connection.scan(options)) {
    c.forEachRemaining(keys::add);
} catch (UnsupportedOperationException e) {
    if (e.getMessage().contains("SSCAN")) { // mislabeled SCAN message
        keys.addAll(connection.keys(pattern)); // small keyspace fallback
    } else throw e;
}

Prevention

When it happens

Trigger: Calling RedisConnection.scan(options) on the redisson-spring-data-18 provider and iterating the Cursor inside executePipelined(...) or a multi/exec block.

Common situations: Key-enumeration utilities (replacing KEYS with SCAN) invoked inside batched/pipelined code; transactional service methods that also iterate keys; upgrading Spring Data Redis versions without re-auditing cursor call sites.

Related errors


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