Skip to content

Redis Streams Imperative creating massive connections that lead to connection problems #3288

Open

Description

@arne-kroeger

When creating a StreamMessageListenerContainer with Spring boot 4.0.1 written in Kotlin the schedulers seem to open massive connections until the connections stack is full and results in the following error:

java.net.BindException: Can't assign requested address

val create = StreamMessageListenerContainer.create(
            connectionFactory,
            StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                .builder()
                .pollTimeout(Duration.ofMillis(blockMillis))
                .targetType(JobRunInfo::class.java)
                .errorHandler { e ->
                    log.error("Redis Stream error", e) }
                .build()
        )

        create.receive(
            Consumer.from(groupName, consumerName),
            StreamOffset.fromStart(streamKey), {
                handler.invoke(it)
            })

create.start()

Full stacktrace:

org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1868) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1799) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.doGetAsyncDedicatedConnection(LettuceConnection.java:968) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getOrCreateDedicatedConnection(LettuceConnection.java:1022) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getAsyncDedicatedConnection(LettuceConnection.java:953) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceStreamCommands.getAsyncDedicatedConnection(LettuceStreamCommands.java:328) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceStreamCommands.xReadGroup(LettuceStreamCommands.java:292) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.lambda$getReadFunction$1(DefaultStreamMessageListenerContainer.java:245) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:413) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:380) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:368) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.lambda$getReadFunction$0(DefaultStreamMessageListenerContainer.java:244) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.stream.StreamPollTask.readRecords(StreamPollTask.java:147) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.stream.StreamPollTask.doLoop(StreamPollTask.java:128) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.stream.StreamPollTask.run(StreamPollTask.java:113) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to localhost/<unresolved>:49952
	at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:63) ~[lettuce-core-6.8.1.RELEASE.jar:6.8.1.RELEASE/ec0535e]
	at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:41) ~[lettuce-core-6.8.1.RELEASE.jar:6.8.1.RELEASE/ec0535e]
	at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:352) ~[lettuce-core-6.8.1.RELEASE.jar:6.8.1.RELEASE/ec0535e]
	at io.lettuce.core.RedisClient.connect(RedisClient.java:220) ~[lettuce-core-6.8.1.RELEASE.jar:6.8.1.RELEASE/ec0535e]
	at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.lambda$getConnection$1(StandaloneConnectionProvider.java:113) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at java.base/java.util.Optional.orElseGet(Optional.java:364) ~[na:na]
	at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.getConnection(StandaloneConnectionProvider.java:113) ~[spring-data-redis-4.0.1.jar:4.0.1]
	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1797) ~[spring-data-redis-4.0.1.jar:4.0.1]
	... 14 common frames omitted
Caused by: io.netty.channel.AbstractChannel$AnnotatedSocketException: Can't assign requested address: localhost/127.0.0.1:49952
Caused by: java.net.BindException: Can't assign requested address
	at java.base/sun.nio.ch.Net.connect0(Native Method) ~[na:na]
	at java.base/sun.nio.ch.Net.connect(Net.java:589) ~[na:na]
	at java.base/sun.nio.ch.Net.connect(Net.java:596) ~[na:na]
	at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:880) ~[na:na]
	at io.netty.util.internal.SocketUtils$3.run(SocketUtils.java:91) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.internal.SocketUtils$3.run(SocketUtils.java:88) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:571) ~[na:na]
	at io.netty.util.internal.SocketUtils.connect(SocketUtils.java:88) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.socket.nio.NioSocketChannel.doConnect(NioSocketChannel.java:315) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.connect(AbstractNioChannel.java:295) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.connect(DefaultChannelPipeline.java:1361) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeConnect(AbstractChannelHandlerContext.java:547) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.AbstractChannelHandlerContext.connect(AbstractChannelHandlerContext.java:526) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.AbstractChannelHandlerContext.connect(AbstractChannelHandlerContext.java:510) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.DefaultChannelPipeline.connect(DefaultChannelPipeline.java:999) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.Channel.connect(Channel.java:297) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.bootstrap.Bootstrap$3.run(Bootstrap.java:264) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:141) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:535) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:201) ~[netty-transport-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.2.9.Final.jar:4.2.9.Final]
	at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

Tested on Mac OS.

I created a repro project: https://github.com/arne-kroeger/demo-redis-repro (here the setup is done via TestController SmartLifecycle start.

When I remove the group configuration Consumer.from(groupName, consumerName) everything works perfect.

Activity

  1. mp911de commented on Jan 6, 2026

    @mp911de
    Member

    Seems that the block timeout is so small that read requests are issued in rapid succession leading to a state where available ports are blocked by TCP connections in FIN_WAIT state.

    Please either increase the block timeout to avoid exhausting the connection address space or use connection pooling.

    We have seen reports however in which XREADGROUP returns immediately (without considering the BLOCK timeout). We weren't however able to get hold of a reproducer that is able to demonstrate the behavior.

  2. arne-kroeger commented on Jan 7, 2026

    @arne-kroeger
    Author

    @mp911de - I have also tried it with 20000 ms and this resulted in the same result.

  3. mp911de commented on Jan 7, 2026

    @mp911de
    Member

    What about pooling?

  4. arne-kroeger commented on Jan 7, 2026

    @arne-kroeger
    Author

    @mp911de - that fixed that problem in the first place regarding this exception but still no data was processed.

    The problem was that I specified for the Consumer Group a stream offset of StreamOffset.fromStart(streamKey). Which does not make any sense. By changing it to StreamOffset.create(streamKey, ReadOffset.lastConsumed()) it fixed the connection problem without specifying a connection pool.

    Seems to be that by specifying the start index it lead to these problems. Maybe it is worth catching this as an assertion case?

  5. mp911de commented on Jan 7, 2026

    @mp911de
    Member

    Thanks for additional details. It likely is a combination that we haven't tested and considered yet and its worth revisiting the arrangement to see whether we can improve here.

  6. arne-kroeger commented on Jan 7, 2026

    @arne-kroeger
    Author

    Thanks for your support. Shall I close this ticket for now or do you want to use it as a reference?

  7. mp911de commented on Jan 7, 2026

    @mp911de
    Member

    Please keep it open, we want to explore how we can do better.

  8. removed their assignment
    on Apr 10, 2026
  9. self-assigned this
    on Apr 27, 2026
  10. jewoodev commented on Jun 2, 2026

    @jewoodev
    Contributor

    Hi @arne-kroeger, I looked into this and I think the diagnosis you arrived at matches the lower-level behavior I found — the combination is being silently lost on the Redis side, then amplified by the polling/connection model on the Spring side.

    Per the Redis docs, when XREADGROUP is called with a history/concrete ID such as 0-0 instead of > (which is what StreamOffset.fromStart(...) resolves to, via ReadOffset.from("0-0")), the command enters the PEL/history mode and BLOCK, NOACK, and CLAIM are ignored; it returns synchronously even with an empty result. This is documented at https://redis.io/docs/latest/commands/xreadgroup/ and visible in t_stream.c's xreadCommand (serve_history=1 → serve_synchronously=1 branch).

    From the current code, Spring Data Redis treats this case as if the call were really blocking:

    • LettuceStreamCommands#xReadGroup acquires a dedicated connection whenever readOptions.isBlocking() is true, regardless of the offset value.
    • StreamPollTask#doLoop polls back-to-back with Thread.sleep(0) between iterations.
    • On a non-pooled LettuceConnectionFactory, RedisTemplate.execute's per-call acquire/release means every poll opens and closes a fresh native connection, which is what exhausts the ephemeral port range and surfaces as BindException.

    So Can't assign requested address is the secondary effect of the silently-ignored BLOCK, not the primary fault.

    I'd be happy to work on a focused fix. My plan would be:

    1. Detect the misconfiguration where readOptions.isBlocking() is true, a Consumer is set, and the offset is a history/concrete stream ID such as 0-0. The natural place is in DefaultStreamMessageListenerContainer — ConsumerStreamReadRequestBuilder itself doesn't see the container's pollTimeout, but the container does.
    2. To keep this compatible with 4.x, where existing users may already be using fromStart(...) or other explicit stream IDs with a Consumer, I'd suggest a deprecation-first step: log a clear WARN that explains the Redis-side ignored BLOCK and points at ReadOffset.lastConsumed(), and only escalate to IllegalArgumentException in 5.0 (or behind an opt-in flag if you prefer it sooner).
    3. Tighten the Javadoc on ReadOffset.from(String), StreamReadOptions#block, and StreamMessageListenerContainerOptions#pollTimeout to note that history/concrete offsets put XREADGROUP into PEL mode where Redis ignores BLOCK.

    I also considered making the polling task reuse a single dedicated connection (which would reduce the connection-churn symptom for this immediate-return polling path), but that changes the LettuceConnection / RedisTemplate callback lifecycle more broadly, so I'd rather start with validation + docs and leave that as a separate discussion if you'd like to pursue it.

    I'd add focused unit coverage for the new WARN/exception; the existing *StreamMessageListenerContainerIntegrationTests paths use lastConsumed() and should stay untouched.

    Please let me know whether you prefer a WARN-first path, an immediate hard fail, or another direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions