{"record":{"id":"fb9e1ae54a0bf94b","repo":"redisson/redisson","slug":"sscan-cannot-be-called-in-pipeline-transaction-fb9e1a","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-23/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java","lineNumber":255,"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":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/redisson/redisson/blob/91188987c2a9023e7fedabff758681ad9b107f25/redisson-spring/redisson-spring-data/redisson-spring-data-23/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java#L237-L273","documentation":"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.","triggerScenarios":"connection.scan(options) called after multi() or inside executePipelined/executeWithSession on a standard (non-cluster) RedissonConnection.","commonSituations":"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.","solutions":["Perform the scan before entering pipeline/transaction mode and collect keys into a list","Do only per-key follow-up commands inside executePipelined","Use redisson.getKeys().getKeysByPattern() or Redisson RKeys API which manages iteration without Spring Data's connection modes"],"exampleFix":"// before\nredisTemplate.executePipelined((RedisCallback<Object>) conn -> {\n    conn.scan(ScanOptions.scanOptions().match(\"user:*\").build())\n       .forEachRemaining(k -> conn.expire(k, 60));\n    return null;\n});\n\n// after\nList<byte[]> keys = new ArrayList<>();\ntry (Cursor<byte[]> c = redisTemplate.getConnectionFactory().getConnection()\n        .scan(ScanOptions.scanOptions().match(\"user:*\").build())) {\n    c.forEachRemaining(keys::add);\n}\nredisTemplate.executePipelined((RedisCallback<Object>) conn -> {\n    keys.forEach(k -> conn.expire(k, 60));\n    return null;\n});","handlingStrategy":"validation","validationCode":"if (connection.isQueueing() || connection.isPipelined()) {\n    throw new IllegalStateException(\"scan requires a plain connection\");\n}\ntry (Cursor<byte[]> c = connection.scan(options)) { ... }","typeGuard":null,"tryCatchPattern":"try { connection.scan(options); } catch (UnsupportedOperationException e) { if (e.getMessage().contains(\"SSCAN\")) { /* rerun on fresh non-pipelined connection */ } else throw e; }","preventionTips":["Structure jobs as scan-phase (plain) + mutate-phase (pipelined)","Review every executePipelined block for Cursor usage before merging","Consider Redisson RKeys.getKeysByPattern() for scan-like iteration"],"tags":["redis","redisson","spring-data-redis","scan","pipeline","transaction"],"backgroundTag":null,"analyzedSha":"91188987c2a9023e7fedabff758681ad9b107f25","analyzedAt":"2026-08-14T11:39:42.619Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}