Skip to content

Intermittent RedisCommandTimeoutException with AWS Valkey Serverless despite low CPU/memory usage

0

We are using AWS Valkey Serverless with a Spring Boot microservices architecture running on Kubernetes.

We intermittently see Redis command timeout exceptions for both GET and SETEX operations, even though Valkey CPU and memory utilization remain low (typically below 30%).

Environment:

  • AWS Valkey Serverless
  • Spring Boot
  • Spring Data Redis
  • Lettuce client
  • Kubernetes
  • SQS-driven async consumers (Spring Cloud AWS SQS)
  • Multiple pods per microservice (2–8 pods depending on scaling)

Example exceptions:

  1. GET timeout:
io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
at io.lettuce.core.protocol.CommandExpiryWriter...
  1. SETEX timeout:
org.springframework.data.redis.connection.lettuce.LettuceStringCommands.setEx(...)
Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
at io.lettuce.core.protocol.CommandExpiryWriter...

Representative stack trace (SETEX example):

org.springframework.dao.QueryTimeoutException: Redis command timed out
...
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.setEx(LettuceStringCommands.java:134)
...
Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
    at io.lettuce.core.internal.ExceptionFactory.createTimeoutException(ExceptionFactory.java:59)
    at io.lettuce.core.protocol.CommandExpiryWriter.lambda$null$0(CommandExpiryWriter.java:179)
    at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:96)
    at io.netty.util.concurrent.DefaultEventExecutor.run(DefaultEventExecutor.java:66)

We see similar stack traces for both GET and SETEX operations.

Important observations:

  • Traffic was continuous during the incident (not idle-period related)
  • Both GET and PUT/SETEX operations are affected
  • Redis CPU and memory metrics were healthy
  • Failures are intermittent
  • Operations themselves are simple key-value cache operations
  • Using synchronous RedisTemplate APIs currently

Relevant Redis/Lettuce configuration (sanitized)

@Bean
public LettuceConnectionFactory redisConnectionFactory(ClientResources clientResources) {

    RedisClusterConfiguration clusterConfig =
            new RedisClusterConfiguration(
                    Collections.singletonList(redisHost + ":" + redisPort));

    SocketOptions socketOptions = SocketOptions.builder()
            .keepAlive(SocketOptions.KeepAliveOptions.builder()
                    .enable(true)
                    .idle(Duration.ofSeconds(30))
                    .interval(Duration.ofSeconds(10))
                    .count(3)
                    .build())
            .connectTimeout(Duration.ofMillis(2000))
            .build();

    ClusterTopologyRefreshOptions topologyRefreshOptions =
            ClusterTopologyRefreshOptions.builder()
                    .enablePeriodicRefresh(Duration.ofSeconds(60))
                    .enableAllAdaptiveRefreshTriggers()
                    .closeStaleConnections(true)
                    .dynamicRefreshSources(true)
                    .build();

    TimeoutOptions timeoutOptions = TimeoutOptions.builder()
            .timeoutSource(new DynamicClusterTimeout(
                    Duration.ofMillis(5000),
                    Duration.ofMillis(2000)))
            .build();

    ClusterClientOptions clientOptions = ClusterClientOptions.builder()
            .topologyRefreshOptions(topologyRefreshOptions)
            .autoReconnect(true)
            .socketOptions(socketOptions)
            .timeoutOptions(timeoutOptions)
            .nodeFilter(node ->
                    !(node.is(RedisClusterNode.NodeFlag.FAIL)
                    || node.is(RedisClusterNode.NodeFlag.EVENTUAL_FAIL)
                    || node.is(RedisClusterNode.NodeFlag.HANDSHAKE)
                    || node.is(RedisClusterNode.NodeFlag.NOADDR)))
            .validateClusterNodeMembership(false)
            .build();

    LettuceClientConfiguration clientConfig =
            LettuceClientConfiguration.builder()
                    .clientOptions(clientOptions)
                    .clientResources(clientResources)
                    .useSsl()
                    .build();

    return new LettuceConnectionFactory(clusterConfig, clientConfig);
}

Would appreciate guidance from anyone who has seen similar behavior with Valkey Serverless + Lettuce in high-concurrency workloads.

2 Answers
0

Low Valkey CPU/memory means the server isn't the bottleneck — the issue is client-side. Two most likely causes:

1. Single multiplexed connection saturated

Lettuce uses one TCP connection by default. With multiple SQS consumers issuing synchronous RedisTemplate calls, commands queue on that single connection. Any network jitter or TLS renegotiation causes the queue to back up → 5s timeout.

Fix — enable connection pooling:

LettucePoolingClientConfiguration clientConfig =
    LettucePoolingClientConfiguration.builder()
        .poolConfig(new GenericObjectPoolConfig<>() {{
            setMaxTotal(16);
            setMinIdle(4);
        }})
        .clientOptions(clientOptions)
        .useSsl()
        .build();

2. Stale topology after slot migration

Valkey Serverless rebalances slots transparently. Your 60s topology refresh means Lettuce routes to stale nodes for up to a minute after a migration → MOVED redirections pile up → timeout.

Fix — reduce refresh interval:

.enablePeriodicRefresh(Duration.ofSeconds(15))  // was 60

Also reduce keepalive idle from 30s to 15s — Valkey Serverless may drop idle connections before your client detects it:

.idle(Duration.ofSeconds(15))
.interval(Duration.ofSeconds(5))

To confirm: check CloudWatch NewConnections (frequent reconnects = connection drops) and CurrConnections (hitting limits = pool exhaustion). Correlate timeout timestamps with any Valkey scaling events.

answered 4 months ago

  • Thanks for the detailed explanation. I tried the suggested changes:

    • enabled Lettuce pooling (maxTotal=16, minIdle=4)
    • reduced topology refresh interval to 15s
    • reduced keepalive idle settings as suggested

    During load testing in lower environment with fewer pods and CPUs with these changes, I observed:

    • NewConnections consistently around 300+/min (sum statistic)
    • CurrConnections mostly staying in the 10s range (maximum statistic)

    For comparison, in our current production setup (without these new changes/load test conditions), we usually observe:

    • NewConnections around 2.2k/min (sum statistic)
    • CurrConnections around 40s (max statistic)

    This made me wonder whether the current behavior during load testing indicates connection churn/reconnect activity rather than pool exhaustion, since the active connection count is still relatively low and not approaching pool limits.

    Would you still consider this expected/healthy behavior for Valkey Serverless with aggressive topology refresh enabled.

0

Based on your configuration and symptoms, here are several areas to investigate for intermittent timeout issues with Valkey Serverless:

1. Client-Side Timeout Configuration

Your current setup uses a 5-second command timeout, which may be appropriate for most operations but could be problematic in certain scenarios. Consider:

  • Differentiate timeouts by operation type: Simple GET operations typically complete quickly, while SETEX and operations involving larger values may need more time. You might want to implement different timeout values for different command types rather than using a fixed 5-second timeout for all operations.

  • Review your DynamicClusterTimeout implementation: Your custom timeout source with 5000ms command timeout and 2000ms connection timeout should be evaluated. The connection timeout (2000ms) is reasonable, but ensure your dynamic timeout logic isn't inadvertently causing issues during high-concurrency periods.

2. Connection Pool and Resource Management

With multiple Kubernetes pods (2-8 per microservice) and SQS-driven async consumers, you may be experiencing connection resource constraints:

  • Implement connection pooling: Ensure you're using proper connection pooling to avoid exhausting available connections. Each pod creating multiple connections without proper pooling can quickly reach connection limits.

  • Monitor connection counts: Check if you're approaching the maximum number of client connections. When the connection limit is reached, new connection attempts will fail or timeout. Use connection pooling and set appropriate timeout values to manage this.

  • Configure clients for direct replica connections: If you're using read replicas, configure your clients to connect directly to them to distribute the connection load.

3. Network and Latency Considerations

Even with low CPU/memory on Valkey, network issues can cause timeouts:

  • Check for network constraints: Verify there are no network bandwidth limitations, packet loss, or latency spikes between your Kubernetes cluster and Valkey Serverless. Network issues can manifest as intermittent timeouts even when the cache itself is healthy.

  • Review security group rules: Ensure security groups allow traffic on the correct port and that there are no intermittent firewall or network policy issues in your Kubernetes environment.

4. Command Execution Patterns

Your timeout errors can occur when the Redis thread is processing complex or long-running commands:

  • Check for slow-running commands: Even though your operations are simple key-value operations, review if there are any occasional slow commands being executed. You can enable and monitor the SLOWLOG to identify commands taking longer than expected.

  • Evaluate command volume: High volumes of commands per unit time can cause queuing and delays. Monitor whether you're sending excessive commands during peak periods, which could lead to timeouts even with healthy resource utilization.

5. Serverless-Specific Considerations

With Valkey Serverless, there may be scaling or cold-start behaviors:

  • Understand serverless scaling patterns: Serverless offerings may have different performance characteristics during scaling events. Monitor for any correlation between timeout occurrences and potential scaling activities.

  • Implement retry logic with backoff: For intermittent failures, implement exponential backoff retry logic in your application to handle transient issues gracefully.

6. Monitoring and Diagnostics

To better diagnose the issue:

  • Enable detailed logging: Configure slowlog-log-slower-than to a value appropriate for your workload (potentially lower than the default 10ms) to capture commands that are taking longer than expected.

  • Monitor replication metrics: If using replication, check ReplicationBytes and ReplicationLag metrics to ensure replication health isn't contributing to delays.

  • Track latency metrics: Monitor command-specific latency metrics, SuccessfulWriteRequestLatency, and SuccessfulReadRequestLatency to understand actual command execution times versus timeout thresholds.

Immediate Actions to Consider:

  1. Review and potentially increase your command timeout for write operations (SETEX) while keeping GET timeouts lower
  2. Verify connection pooling is properly configured across all your pods
  3. Monitor actual connection counts against available limits
  4. Enable SLOWLOG and review for any unexpectedly slow commands
  5. Check for network latency or packet loss between Kubernetes and Valkey
  6. Implement retry logic with exponential backoff for transient failures

The intermittent nature of your timeouts, combined with healthy CPU/memory metrics, suggests the issue is likely related to connection management, network latency, or occasional command queuing rather than resource exhaustion on the Valkey side.
Sources
Troubleshoot error messages in ElastiCache Redis clients | AWS re:Post
Configure a client-side timeout (Valkey and Redis OSS) - Amazon ElastiCache
Amazon ElastiCache Well-Architected Lens Performance Efficiency Pillar - Amazon ElastiCache

answered 4 months ago

EXPERT

reviewed 4 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.