Clients & Client-Side Caching

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client libraries, connecting to the same Bookshop keyspace used throughout this section (bookshop:book:<isbn> and friends).

Choosing a client

Jedis 8 is a synchronous, blocking client built around RedisClient — the API that, from Jedis 7.2.0 onward, replaces the older JedisPool/JedisPooled entry points. These are still present, and UnifiedJedis remains RedisClient’s base class. Jedis 8 also provides `RedisClusterClient and RedisSentinelClient for cluster and Sentinel topologies. It suits code that already runs synchronously end to end: a servlet-style request handler, a batch job, or any call site that would otherwise have to bridge back to blocking I/O anyway. Lettuce 7.7 is built on Netty and is asynchronous and reactive first, with a synchronous facade (.sync()) layered on top of the same connection — it suits reactive stacks, high connection-concurrency services, and is Spring Boot’s default Redis client (spring-boot-starter-data-redis pulls in Lettuce, not Jedis) when Spring Data Redis is on the classpath. See Jedis guide and Lettuce guide.

Redis also ships officially supported clients for other languages — named here for completeness, not elaborated on this Java-focused page: redis-py (Python), node-redis (Node.js), go-redis (Go), and NRedisStack (.NET).

Pools, timeouts, and reliability

Jedis: pooling and timeouts

From Jedis 7.2.0, RedisClient pools connections internally — pass it a ConnectionPoolConfig (a wrapper around Apache Commons Pool2’s GenericObjectPoolConfig) to size the pool, and a DefaultJedisClientConfig to set the socket and connection timeouts:

package com.example.bookshop;

import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.csc.CacheConfig;
import redis.clients.jedis.ConnectionPoolConfig;

import java.time.Duration;

public class BookshopJedisConfig {

    public RedisClient bookshopRedisClient() {
        ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
        poolConfig.setMaxTotal(16);
        poolConfig.setMaxIdle(16);
        poolConfig.setMinIdle(2);
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxWait(Duration.ofSeconds(1));
        poolConfig.setTestWhileIdle(true);
        poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));

        DefaultJedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
                .protocol(RedisProtocol.RESP3)
                .socketTimeoutMillis(2000)
                .connectionTimeoutMillis(2000)
                .user("bookshop-app")
                .password("changeit")
                .build();

        return RedisClient.builder()
                .hostAndPort(new HostAndPort("redis.internal.bookshop.example", 6379))
                .clientConfig(clientConfig)
                .poolConfig(poolConfig)
                .build();
    }
}

RESP3 (set with .protocol(RedisProtocol.RESP3) above) is required for both push-based invalidation messages (client-side caching, below) and for richer typed replies in general, and is the protocol Jedis and Lettuce both default new connections toward on modern Redis servers. Retries after a dropped connection are the caller’s responsibility with Jedis — a lost connection surfaces as JedisConnectionException, and production code wraps the call in a bounded retry loop with backoff, requesting a fresh pooled connection on each attempt. See Retrying a command after a connection failure.

For TLS, build a JedisClientConfig with an SslOptions truststore/keystore alongside the ACL user/ password; for reading from replicas or coordinating failover, point RedisClient at a Sentinel- or cluster-aware variant (RedisSentinelClient/RedisClusterClient) rather than a single HostAndPort, so the client discovers the current primary (and, for Sentinel, can be pointed at replicas for read traffic) instead of hardcoding one node. See Connect with TLS (Jedis) and ACL.

Lettuce: ClientOptions, timeouts, and reconnection

Lettuce multiplexes many logical connections over one physical connection by default, so "pooling" in the Jedis sense is less often needed — a single StatefulRedisConnection handles concurrent callers safely. ClientOptions (plus SocketOptions for TCP-level settings) configures timeouts, keepalive, and reconnect behavior:

package com.example.bookshop;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;

import java.time.Duration;

public class BookshopLettuceConfig {

    public RedisClient bookshopRedisClient() {
        RedisURI redisUri = RedisURI.Builder
                .redis("redis.internal.bookshop.example", 6379)
                .withAuthentication("bookshop-app", "changeit".toCharArray())
                .withTimeout(Duration.ofSeconds(2))
                .build();

        RedisClient client = RedisClient.create(redisUri);

        SocketOptions socketOptions = SocketOptions.builder()
                .connectTimeout(Duration.ofSeconds(2))
                .keepAlive(true)
                .build();

        client.setOptions(ClientOptions.builder()
                .socketOptions(socketOptions)
                .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(2)))
                .autoReconnect(true)
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.DEFAULT)
                .build());

        return client;
    }
}

This derives from Lettuce production usage and the Lettuce project. autoReconnect(true) (the default) gives at-least-once command execution — Lettuce reconnects automatically and replays commands that were queued when the connection dropped; autoReconnect(false) switches to at-most-once, discarding queued commands instead. RedisURI also carries TLS (.withSsl(true)) and ACL credentials (.withAuthentication(user, password)) the same way it carries the timeout above. For reading from replicas, StatefulRedisConnection.sync()/.async() accept a ReadFrom setting (for example ReadFrom.REPLICA_PREFERRED) so read-only commands can be routed off the primary; for failover, RedisClient reacts to Sentinel or Cluster topology changes on its own once connected through RedisSentinelClient semantics via RedisURI.Builder.sentinel(…​) or a RedisClusterClient, redirecting subsequent commands to the newly elected primary. See Lettuce production usage.

Client-side caching

Client-side caching lets the client keep a local copy of recently read keys and skip the round trip to Redis on a cache hit. The server enables it per connection with CLIENT TRACKING ON: in the default (non-broadcast) mode, Redis tracks only the keys that connection actually read and sends an invalidation message — over the same RESP3 connection, or redirected to another connection under RESP2/Pub-Sub — the moment any client writes to one of them; the receiving client then evicts that key from its local cache rather than serving stale data on the next read. Broadcast mode (CLIENT TRACKING ON BCAST [PREFIX prefix …​]) instead sends invalidation messages for every key matching a subscribed prefix, regardless of whether that connection ever read it — useful when many clients share the same small set of hot prefixes and per-key tracking overhead isn’t worth it. See Client-side caching introduction and CLIENT TRACKING.

Jedis (v5.2.0+, so all of Jedis 8) supports the default tracking mode out of the box through CacheConfig; broadcast and opt-in/opt-out modes are server-side features Jedis does not currently implement on top of. Lettuce supports client-side caching through its io.lettuce.core.support.caching package, which wires up tracking and invalidation handling around a pluggable local cache (for example a ConcurrentHashMap).

Jedis client-side caching, scoped to the Bookshop bookshop:cache:book:<isbn> read path:

package com.example.bookshop;

import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.csc.Cache;
import redis.clients.jedis.csc.CacheConfig;

public class BookshopCachingBookRepository {

    private final RedisClient client;

    public BookshopCachingBookRepository() {
        DefaultJedisClientConfig config = DefaultJedisClientConfig.builder()
                .protocol(RedisProtocol.RESP3)
                .build();

        CacheConfig cacheConfig = CacheConfig.builder()
                .maxSize(5_000)
                .build();

        this.client = RedisClient.builder()
                .hostAndPort(new HostAndPort("redis.internal.bookshop.example", 6379))
                .clientConfig(config)
                .cacheConfig(cacheConfig)
                .build();
    }

    public String findTitle(String isbn) {
        // First call per isbn hits Redis and populates the cache; later calls
        // for the same isbn are served locally until the key is invalidated.
        return client.hget("bookshop:book:" + isbn, "title");
    }

    public void evict(String isbn) {
        Cache cache = client.getCache();
        cache.deleteByRedisKey("bookshop:book:" + isbn);
    }
}
sequenceDiagram participant App as Bookshop app (Jedis client) participant Redis as Redis server participant Writer as Another client App->>Redis: HGET bookshop:book:9780000000001 title Redis-->>App: "Streams in Practice" Note over App: Value cached locally,
key now tracked by server App->>Redis: HGET bookshop:book:9780000000001 title Note over App,Redis: Served from local cache,
no round trip Writer->>Redis: HSET bookshop:book:9780000000001 title "..." Redis-->>App: Invalidation push message Note over App: Local cache entry evicted App->>Redis: HGET bookshop:book:9780000000001 title Redis-->>App: Fresh value, re-cached

Object mapping and migration tooling

Redis OM (Object Mapping) layers repository- and annotation-based mapping on top of a client — for the Bookshop domain, Java entities that map onto the bookshop:book:* hashes/JSON documents used elsewhere in this section, backed by the same search index covered in Query Engine: Indexing & Data Modeling. Its full treatment, together with Spring Data Redis repositories and Lua-backed custom queries, lives in Spring Boot: Spring Data Redis, Redis OM & Custom Queries.

For one-off or recurring bulk migration into or out of Redis — for example seeding the Bookshop keyspace from a relational export, or replicating between two Redis deployments — RIOT (Redis Input/Output Tool) is the dedicated command-line tool: it reads from and writes to Redis alongside files, databases, and other Redis instances, without hand-rolling a client-side import script. See RIOT.

References