{
  "schema_version": 2,
  "id": "develop/clients/lettuce/produsage",
  "title": "Production usage",
  "url": "https://redis.io/docs/latest/develop/clients/lettuce/produsage/",
  "summary": "Get your Lettuce app ready for production",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc",
    "oss",
    "kubernetes",
    "clients"
  ],
  "last_updated": "2026-10-05T16:11:44+01:00",
  "page_type": "content",
  "content_hash": "47e0e14e4dc97e0b554be802c8870b86a61bbdf45dd2e56977c1acf0ba19741d",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide offers recommendations to get the best reliability and\nperformance in your production environment."
    },
    {
      "id": "checklist",
      "title": "Checklist",
      "role": "content",
      "text": "Each item in the checklist below links to the section\nfor a recommendation. Use the checklist icons to record your\nprogress in implementing the recommendations.\n\n[code example]"
    },
    {
      "id": "recommendations",
      "title": "Recommendations",
      "role": "content",
      "text": "The sections below offer recommendations for your production environment. Some\nof them may not apply to your particular use case."
    },
    {
      "id": "timeouts",
      "title": "Timeouts",
      "role": "content",
      "text": "Lettuce provides timeouts for many operations, such as command execution, SSL handshake, and Sentinel discovery. By default, Lettuce uses a global timeout value of 60 seconds for these operations, but you can override the global timeout value with individual timeout values for each operation.\n\n> [!TIP]\n> Choosing suitable timeout values is crucial for your application's performance and stability and is specific to each environment.\n> Configuring timeouts is only necessary if you have issues with the default values. \n> In some cases, the defaults are based on environment-specific settings (e.g., operating system settings), while in other cases, they are built into the Lettuce driver. \n> For more details on setting specific timeouts, see the [Lettuce reference guide](https://redis.github.io/lettuce/)."
    },
    {
      "id": "prerequisites",
      "title": "Prerequisites",
      "role": "content",
      "text": "To set TCP-level timeouts, you need to ensure you have one of [Netty Native Transports](https://netty.io/wiki/native-transports.html) installed. The most common one is `netty-transport-native-epoll`, which is used for Linux systems. You can add it to your project by including the following dependency in your `pom.xml` file:\n\n[code example]\n\nOnce you have the native transport dependency, you can verify that by using the following code:\n\n[code example]\n\nIf the snippet above returns `false`, you need to enable debugging logging for `io.lettuce.core` and `io.netty` to see why the native transport is not available.\n\nFor more information on using Netty Native Transport, see the [Lettuce reference guide](https://redis.github.io/lettuce/advanced-usage/native-transports/)."
    },
    {
      "id": "setting-timeouts",
      "title": "Setting timeouts",
      "role": "configuration",
      "text": "Below is an example of setting socket-level timeouts. The `TCP_USER_TIMEOUT` setting is useful for scenarios where the server stops responding without acknowledging the last request, while the `KEEPALIVE` setting is good for detecting dead connections where there is no traffic between the client and the server.\n\n[code example]"
    },
    {
      "id": "setting-timeouts-in-spring-data-redis",
      "title": "Setting timeouts in Spring Data Redis",
      "role": "configuration",
      "text": "If you are using Spring Data Redis, you can set timeouts and keepalive settings using `LettuceClientConfigurationBuilderCustomizer`:\n\n[code example]"
    },
    {
      "id": "cluster-topology-refresh",
      "title": "Cluster topology refresh",
      "role": "content",
      "text": "The Redis Cluster configuration is dynamic and can change at runtime. \nNew nodes may be added, and the primary node for a specific slot can shift.\nLettuce automatically handles [MOVED](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#moved-redirection) and [ASK](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#ask-redirection) redirects, but to enhance your application's resilience, you should enable adaptive topology refreshing:\n\n[code example]\nLearn more about topology refresh configuration settings in [the reference guide](https://redis.github.io/lettuce/ha-sharding/#redis-cluster)."
    },
    {
      "id": "warm-up-cluster-connections",
      "title": "Warm up cluster connections",
      "role": "content",
      "text": "With a Redis Cluster, Lettuce opens connections to individual nodes *lazily* -\nthe connection to a given shard is created the first time a command is routed\nto it. This keeps the connection footprint minimal, but it means the *first*\nrequests after startup each pay the cost of establishing a new connection\n(a TCP connection plus, when TLS is enabled, a TLS handshake) to a node that\nhas not been contacted yet. On a TLS cluster with several shards, this can add\na noticeable latency spike to a freshly started application's first burst of\ntraffic. Under constrained CPU (for example, a container that is CPU-throttled\nduring startup) that spike can be large enough to breach command timeouts.\n\nTo avoid this, open the per-node connections *before* your application starts\nserving traffic. The `upstream()` node selection targets every primary node;\nsending a `PING` to the selection forces each per-node connection to be\nestablished:\n\n[code example]\n\nBecause the cluster topology can change at runtime, connections to *new* nodes\nare still opened lazily after a topology change. If you want those warmed as\nwell, re-run the warm-up when the topology changes (for example, from a\n`ClusterTopologyChangedEvent` listener)."
    },
    {
      "id": "warming-up-connections-in-spring-data-redis",
      "title": "Warming up connections in Spring Data Redis",
      "role": "content",
      "text": "With [Spring Data Redis](https://redis.io/docs/latest/integrate/spring-framework-cache), run the warm-up once at startup, before the instance is\nmarked ready. Obtain the shared native cluster connection from the\n`LettuceConnectionFactory` and warm it with the same `upstream()` call:\n\n[code example]\n\n> [!NOTE]\n> The `LettuceConnectionFactory` `eagerInitialization` option is not\n> sufficient on its own. It establishes the cluster topology and a single\n> connection at startup, but the remaining per-node connections are still opened\n> lazily on first use. Use the warm-up shown above to open connections to all\n> nodes."
    },
    {
      "id": "dns-cache-and-redis",
      "title": "DNS cache and Redis",
      "role": "content",
      "text": "When you connect to a Redis server with multiple endpoints, such as [Redis Software Active-Active](https://redis.com/redis-enterprise/technology/active-active-geo-distribution/), you *must*\ndisable the JVM's DNS cache. If a server node or proxy fails, the IP address for any database\naffected by the failure will change. When this happens, your app will keep\ntrying to use the stale IP address if DNS caching is enabled.\n\nUse the following code to disable the DNS cache:\n\n[code example]"
    },
    {
      "id": "exception-handling",
      "title": "Exception handling",
      "role": "errors",
      "text": "Redis handles many errors using return values from commands, but there\nare also situations where exceptions can be thrown. In production code,\nyou should handle exceptions as they occur.\n\nSee the Error handling sections of the\n[Lettuce async](https://redis.github.io/lettuce/user-guide/async-api/#error-handling) and\n[Lettuce reactive](https://redis.github.io/lettuce/user-guide/reactive-api/#error-handling)\nAPI guides to learn more about handling exceptions."
    },
    {
      "id": "connection-and-execution-reliability",
      "title": "Connection and execution reliability",
      "role": "content",
      "text": "By default, Lettuce uses an *at-least-once* strategy for command execution.\nIt will automatically reconnect after a disconnection and resume executing\nany commands that were queued when the connection was lost. If you\nswitch to *at-most-once* execution, Lettuce will\nnot reconnect after a disconnection and will discard commands\ninstead of queuing them. You can enable at-most-once execution by setting\n`autoReconnect(false)` in the\n`ClientOptions` when you create the client, as shown in the example below:\n\n[code example]\n\nIf you need finer control over which commands you want to execute in which mode, you can\nconfigure a *replay filter* to choose the commands that should retry after a disconnection.\nThe example below shows a filter that retries all commands except for\n[`DECR`](https://redis.io/docs/latest/commands/decr)\n(this command is not [idempotent](https://en.wikipedia.org/wiki/Idempotence) and\nso you might need to avoid executing it more than once). Note that\nreplay filters are only available in Lettuce v6.6 and above.\n\n[code example]\n\nSee\n[Command execution reliability](https://redis.github.io/lettuce/advanced-usage/command-execution-reliability/)\nin the Lettuce reference guide for more information."
    },
    {
      "id": "smart-client-handoffs",
      "title": "Smart client handoffs",
      "role": "content",
      "text": "*Smart client handoffs (SCH)* is a feature of Redis Cloud and\nRedis Software servers that lets them actively notify clients\nabout planned server maintenance shortly before it happens. This\nlets a client take action to avoid disruptions in service.\n\nSee [Smart client handoffs](https://redis.io/docs/latest/develop/clients/sch)\nfor more information about SCH and\n[Connect using Smart client handoffs](https://redis.io/docs/latest/develop/clients/lettuce/connect#connect-using-smart-client-handoffs-sch)\nfor example code."
    }
  ],
  "examples": [
    {
      "id": "checklist-ex0",
      "language": "checklist {id=\"lettuceprodlist\"}",
      "code": "- [ ] [Timeouts](#timeouts)\n- [ ] [Cluster topology refresh](#cluster-topology-refresh)\n- [ ] [Warm up cluster connections](#warm-up-cluster-connections)\n- [ ] [DNS cache and Redis](#dns-cache-and-redis)\n- [ ] [Exception handling](#exception-handling)\n- [ ] [Connection and execution reliability](#connection-and-execution-reliability)\n- [ ] [Smart client handoffs](#seamless-client-experience)",
      "section_id": "checklist"
    },
    {
      "id": "prerequisites-ex0",
      "language": "xml",
      "code": "<dependency>\n    <groupId>io.netty</groupId>\n    <artifactId>netty-transport-native-epoll</artifactId>\n    <version>${netty.version}</version> <!-- e.g., 4.1.118.Final -->\n    <classifier>linux-x86_64</classifier>\n</dependency>",
      "section_id": "prerequisites"
    },
    {
      "id": "prerequisites-ex1",
      "language": "java",
      "code": "logger.info(\"Lettuce epool is available: {}\", EpollProvider.isAvailable());",
      "section_id": "prerequisites"
    },
    {
      "id": "setting-timeouts-ex0",
      "language": "java",
      "code": "RedisURI redisURI = RedisURI.Builder\n        .redis(\"localhost\")\n        // set the global default from the default 60 seconds to 30 seconds\n        .withTimeout(Duration.ofSeconds(30)) \n        .build();\n\ntry (RedisClient client = RedisClient.create(redisURI)) {\n    // or set specific timeouts for things such as the TCP_USER_TIMEOUT and TCP_KEEPALIVE\n\n    // A good general rule of thumb is to follow the rule\n    // TCP_USER_TIMEOUT = TCP_KEEP_IDLE+TCP_KEEPINTVL * TCP_KEEPCNT\n    // in this case, 20 = 5 + 5 * 3\n\n    SocketOptions.TcpUserTimeoutOptions tcpUserTimeout = SocketOptions.TcpUserTimeoutOptions.builder()\n            .tcpUserTimeout(Duration.ofSeconds(20))\n            .enable().build();\n\n    SocketOptions.KeepAliveOptions keepAliveOptions = SocketOptions.KeepAliveOptions.builder()\n            .interval(Duration.ofSeconds(5))\n            .idle(Duration.ofSeconds(5))\n            .count(3).enable().build();\n\n    SocketOptions socketOptions = SocketOptions.builder()\n            .tcpUserTimeout(tcpUserTimeout)\n            .keepAlive(keepAliveOptions)\n            .build();\n\n    client.setOptions(ClientOptions.builder()\n            .socketOptions(socketOptions)\n            .build());\n\n    StatefulRedisConnection<String, String> connection = client.connect();\n    System.out.println(connection.sync().ping());\n}",
      "section_id": "setting-timeouts"
    },
    {
      "id": "setting-timeouts-in-spring-data-redis-ex0",
      "language": "java",
      "code": "@Bean\npublic LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {\n    return clientConfigurationBuilder -> {\n        // Configure TCP User Timeout\n        // This is useful for scenarios where the server stops responding without\n        // acknowledging the last request\n        SocketOptions.TcpUserTimeoutOptions tcpUserTimeout = SocketOptions.TcpUserTimeoutOptions.builder()\n                .tcpUserTimeout(Duration.ofSeconds(20))\n                .enable()\n                .build();\n\n        // Configure TCP Keep-Alive\n        // This is good for detecting dead connections where there is no traffic\n        // between the client and the server\n        SocketOptions.KeepAliveOptions keepAliveOptions = SocketOptions.KeepAliveOptions.builder()\n                .interval(Duration.ofSeconds(5))  // TCP_KEEPINTVL: interval between probes\n                .idle(Duration.ofSeconds(5))      // TCP_KEEPIDLE: time before first probe\n                .count(3)                         // TCP_KEEPCNT: number of probes\n                .enable()\n                .build();\n\n        // Build SocketOptions with both TCP User Timeout and Keep-Alive\n        SocketOptions socketOptions = SocketOptions.builder()\n                .tcpUserTimeout(tcpUserTimeout)\n                .keepAlive(keepAliveOptions)\n                .build();\n\n        // Build ClientOptions with the configured SocketOptions\n        ClientOptions clientOptions = ClientOptions.builder()\n                .socketOptions(socketOptions)\n                .build();\n\n        // Apply the client options and command timeout to the builder\n        clientConfigurationBuilder\n                .clientOptions(clientOptions)\n                .commandTimeout(Duration.ofSeconds(30));  // Global command timeout\n    };\n}",
      "section_id": "setting-timeouts-in-spring-data-redis"
    },
    {
      "id": "cluster-topology-refresh-ex0",
      "language": "java",
      "code": "RedisURI redisURI = RedisURI.Builder\n        .redis(\"localhost\")\n        // set the global default from the default 60 seconds to 30 seconds\n        .withTimeout(Duration.ofSeconds(30)) \n        .build();\n        \n// Create a RedisClusterClient with adaptive topology refresh\ntry (RedisClusterClient clusterClient = RedisClusterClient.create(redisURI)) {\n    // Enable TCP keep-alive and TCP user timeout just like in the standalone example\n    SocketOptions.TcpUserTimeoutOptions tcpUserTimeout = SocketOptions.TcpUserTimeoutOptions.builder()\n            .tcpUserTimeout(Duration.ofSeconds(20))\n            .enable()\n            .build();\n\n    SocketOptions.KeepAliveOptions keepAliveOptions = SocketOptions.KeepAliveOptions.builder()\n            .interval(Duration.ofSeconds(5))\n            .idle(Duration.ofSeconds(5))\n            .count(3)\n            .enable()\n            .build();\n\n    SocketOptions socketOptions = SocketOptions.builder()\n            .tcpUserTimeout(tcpUserTimeout)\n            .keepAlive(keepAliveOptions)\n            .build();\n\n    // Enable adaptive topology refresh\n    // Configure adaptive topology refresh options\n    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()\n            .enableAllAdaptiveRefreshTriggers()\n            .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))\n            .build();\n    \n    ClusterClientOptions options = ClusterClientOptions.builder()\n            .topologyRefreshOptions(topologyRefreshOptions)\n            .socketOptions(socketOptions).build();\n\n    clusterClient.setOptions(options);\n\n    StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();\n    System.out.println(connection.sync().ping());\n    connection.close();\n}",
      "section_id": "cluster-topology-refresh"
    },
    {
      "id": "warm-up-cluster-connections-ex0",
      "language": "java",
      "code": "RedisURI redisURI = RedisURI.Builder\n        .redis(\"localhost\")\n        .withSsl(true)\n        .build();\n\ntry (RedisClusterClient clusterClient = RedisClusterClient.create(redisURI)) {\n\n    StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();\n\n    // Warm up: open a connection to every primary node before serving traffic.\n    // upstream() selects all primaries; the PING forces each per-node connection to open.\n    connection.sync().upstream().commands().ping();\n\n    // If you read from replicas (ReadFrom.REPLICA / REPLICA_PREFERRED),\n    // warm the replica connections too:\n    // connection.sync()\n    //         .readonly(node -> node.is(RedisClusterNode.NodeFlag.REPLICA))\n    //         .commands().ping();\n\n    System.out.println(connection.sync().ping());\n}",
      "section_id": "warm-up-cluster-connections"
    },
    {
      "id": "warming-up-connections-in-spring-data-redis-ex0",
      "language": "java",
      "code": "@Component\nclass RedisClusterWarmUp {\n\n    private final LettuceConnectionFactory factory;\n\n    RedisClusterWarmUp(RedisConnectionFactory factory) {\n        this.factory = (LettuceConnectionFactory) factory;\n    }\n\n    // Runs after the context starts. To keep traffic off the instance until the\n    // warm-up completes, gate your readiness probe on it (for example, with a\n    // HealthIndicator that reports \"up\" only after this method succeeds).\n    @EventListener(ApplicationStartedEvent.class)\n    void warmUp() {\n        try (RedisClusterConnection clusterConnection = factory.getClusterConnection()) {\n            @SuppressWarnings(\"unchecked\")\n            StatefulRedisClusterConnection<byte[], byte[]> connection =\n                    ((RedisAdvancedClusterAsyncCommands<byte[], byte[]>) clusterConnection.getNativeConnection())\n                            .getStatefulConnection();\n            connection.sync().upstream().commands().ping();\n        }\n    }\n}",
      "section_id": "warming-up-connections-in-spring-data-redis"
    },
    {
      "id": "dns-cache-and-redis-ex0",
      "language": "java",
      "code": "java.security.Security.setProperty(\"networkaddress.cache.ttl\",\"0\");\njava.security.Security.setProperty(\"networkaddress.cache.negative.ttl\", \"0\");",
      "section_id": "dns-cache-and-redis"
    },
    {
      "id": "connection-and-execution-reliability-ex0",
      "language": "java",
      "code": "RedisURI uri = RedisURI.Builder\n                .redis(\"localhost\", 6379)\n                .withAuthentication(\"default\", \"yourPassword\")\n                .build();\n\nRedisClient client = RedisClient.create(uri);\n\nclient.setOptions(ClientOptions.builder()\n    .autoReconnect(false)\n        .\n        .\n    .build());",
      "section_id": "connection-and-execution-reliability"
    },
    {
      "id": "connection-and-execution-reliability-ex1",
      "language": "java",
      "code": "Predicate<RedisCommand<?, ?, ?> > filter =\n        cmd -> cmd.getType().toString().equalsIgnoreCase(\"DECR\");\n\nclient.setOptions(ClientOptions.builder()\n    .replayFilter(filter)\n    .build());",
      "section_id": "connection-and-execution-reliability"
    }
  ]
}
