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
- Run scan() on a plain, non-pipelined connection
- Use RedisTemplate.scan(options), which binds its own connection
- 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
- Use RedisTemplate.scan(options) at the template level
- Never iterate cursors inside pipelined or transactional callbacks
- Remember the message text says SSCAN for SCAN — key on connection mode
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
- 'SSCAN' cannot be called in pipeline / transaction mode.
- Unable to locate Redisson instance by name: ${jndiName}
- Unable to locate Redisson instance by name: ${jndiName}
- 'SSCAN' cannot be called in pipeline / transaction mode.
- 'ZSCAN' cannot be called in pipeline / transaction mode.
AI-assisted analysis of redisson/redisson@91188987c2 (2026-08-14).
Data as JSON: /api/errors/91738316f9df7e3f.
Report an issue: GitHub.