{"record":{"id":"91738316f9df7e3f","repo":"redisson/redisson","slug":"sscan-cannot-be-called-in-pipeline-transaction-917383","errorCode":null,"errorMessage":"'SSCAN' cannot be called in pipeline / transaction mode.","messagePattern":"'SSCAN' cannot be called in pipeline / transaction mode\\.","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"redisson-spring/redisson-spring-data/redisson-spring-data-18/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java","lineNumber":267,"sourceCode":"        CompletableFuture<Void> ff = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));\n        CompletableFuture<Set<byte[]>> future = ff.thenApply(r -> {\n            return futures.stream().flatMap(f -> f.getNow(new HashSet<>()).stream()).collect(Collectors.toSet());\n        }).toCompletableFuture();\n        return sync(new CompletableFutureWrapper<>(future));\n    }\n\n    @Override\n    public Cursor<byte[]> scan(ScanOptions options) {\n        return new ScanCursor<byte[]>(0, options) {\n\n            private RedisClient client;\n            private Iterator<MasterSlaveEntry> entries = executorService.getConnectionManager().getEntrySet().iterator();\n            private MasterSlaveEntry entry = entries.next();\n            \n            @Override\n            protected ScanIteration<byte[]> doScan(long cursorId, ScanOptions options) {\n                if (isQueueing() || isPipelined()) {\n                    throw new UnsupportedOperationException(\"'SSCAN' cannot be called in pipeline / transaction mode.\");\n                }\n\n                if (entry == null) {\n                    return null;\n                }\n\n                List<Object> args = new ArrayList<Object>();\n                if (cursorId == 101010101010101010L) {\n                    cursorId = 0;\n                }\n                args.add(Long.toUnsignedString(cursorId));\n                if (options.getPattern() != null) {\n                    args.add(\"MATCH\");\n                    args.add(options.getPattern());\n                }\n                if (options.getCount() != null) {\n                    args.add(\"COUNT\");\n                    args.add(options.getCount());","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/redisson/redisson/blob/91188987c2a9023e7fedabff758681ad9b107f25/redisson-spring/redisson-spring-data/redisson-spring-data-18/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java#L249-L285","documentation":"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.","triggerScenarios":"Calling RedisConnection.scan(options) on the redisson-spring-data-18 provider and iterating the Cursor inside executePipelined(...) or a multi/exec block.","commonSituations":"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.","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"],"exampleFix":"// before\nredisTemplate.executePipelined((RedisCallback<Object>) conn -> {\n    Cursor<byte[]> c = conn.scan(options);\n    c.forEachRemaining(keys::add);\n    return null;\n});\n\n// after\ntry (Cursor<byte[]> c = redisTemplate.scan(options)) {\n    c.forEachRemaining(k -> keys.add(k));\n}","handlingStrategy":"validation","validationCode":"if (connection.isPipelined() || connection.isQueueing()) {\n    throw new IllegalStateException(\"scan() cannot run in pipeline/tx\");\n}\nCursor<byte[]> cursor = connection.scan(options);","typeGuard":"boolean canScan(RedisConnection conn) {\n    return !conn.isPipelined() && !conn.isQueueing();\n}","tryCatchPattern":"try (Cursor<byte[]> c = connection.scan(options)) {\n    c.forEachRemaining(keys::add);\n} catch (UnsupportedOperationException e) {\n    if (e.getMessage().contains(\"SSCAN\")) { // mislabeled SCAN message\n        keys.addAll(connection.keys(pattern)); // small keyspace fallback\n    } else throw e;\n}","preventionTips":["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"],"tags":["redis","scan","cursor","pipeline","transaction","spring-data-redis"],"backgroundTag":null,"analyzedSha":"91188987c2a9023e7fedabff758681ad9b107f25","analyzedAt":"2026-08-14T11:39:42.619Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}