Jedis: A Java Developer's Guide to the Redis Client
Nobody gets Jedis by accident. spring-boot-starter-data-redis ships Lettuce, so if you are running Jedis you chose it, and you probably chose it because it is small, blocking, and does exactly what you tell it. That is still true. What is no longer true is most of what you will find written about it.
In December 2025, Jedis 7.2.0 deprecated JedisPool, JedisPooled, JedisCluster and JedisSentinelPool in favour of a new family of client classes. In August 2026, Jedis 8.0.0 deleted JedisPooled outright, turned on RESP3 by default, and made TLS hostname verification strict. Every tutorial on the first page of Google for this client predates at least one of those changes, and the most popular one predates all three by two and a half years.
This guide is about the client as it exists in 8.0.1, and about the handful of defaults that decide how it behaves when something goes wrong. Everything here applies equally to Valkey. Redisson, which we build, is a different kind of client for the same server, and the comparison waits until the end.
Which Jedis Are You Actually On?
Open your POM before you read any further. The version you are on decides which half of this guide applies to you.
| Version | Released | What changed |
|---|---|---|
| 6.2.0 | — | The last release where nothing is deprecated. JedisPool, JedisPooled and JedisCluster are the supported API. |
| 7.0.0 | 10 Oct 2025 | Sharding classes removed. PipelineBase becomes AbstractPipeline, TransactionBase becomes AbstractTransaction. MultiDbClient arrives. |
| 7.2.0 | 17 Dec 2025 | RedisClient, RedisClusterClient and RedisSentinelClient arrive. The four older classes are marked @Deprecated. |
| 8.0.0 | 10 Aug 2026 | JedisPooled and JedisSentineled removed. RESP3 negotiated by default. TLS hostname verification enforced. |
| 8.0.1 | 28 Aug 2026 | Current release. |
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
</dependency>
Jedis still targets Java 8 as a baseline, so the upgrade is almost never blocked by your JDK. The thing that blocks it is source compatibility, and the rest of this guide is largely about where.
If you are on 5.x or 6.x, nothing in your code is broken yet. If you are on 7.x, your build is emitting deprecation warnings you may have muted. If you are on 8.x and your build still compiles, you were not using JedisPooled.
The RedisClient Family
Jedis 7.2.0 introduced four classes that replace the older entry points. The javadoc is unambiguous about intent:
"RedisClientsupersedesJedisandUnifiedJedisclasses, offering improved usability and extensibility. For new applications, useRedisClientinstead of the older classes."
| Deployment | Old | New |
|---|---|---|
| Standalone | Jedis, JedisPool, JedisPooled, UnifiedJedis | RedisClient |
| Cluster | JedisCluster | RedisClusterClient |
| Sentinel | JedisSentinelPool, JedisSentineled | RedisSentinelClient |
| Multiple endpoints with failover | — | MultiDbClient |
The simple case is a static factory:
RedisClient client = RedisClient.create("localhost", 6379);
client.set("book:978-0134685991", "Effective Java");
String title = client.get("book:978-0134685991");
client.close();
There are factories for a HostAndPort, for a redis:// or rediss:// URL, and for host, port, user and password. Anything beyond that goes through the builder, which is where the rest of this guide happens:
RedisClient client = RedisClient.builder()
.hostAndPort("redis.internal", 6379)
.clientConfig(DefaultJedisClientConfig.builder()
.user("app")
.password("secret")
.clientName("orders-service")
.connectionTimeoutMillis(2000)
.socketTimeoutMillis(2000)
.build())
.poolConfig(poolConfig)
.build();
A RedisClient is closeable and holds a pool underneath. Build one per Redis deployment, keep it for the life of the application, and close it on shutdown. It is not a per-request object.
One naming trap worth flagging before it costs you an hour. Lettuce has had a class called RedisClient since its first release, in the package io.lettuce.core. Jedis now has a different class with the same name, in redis.clients.jedis. If your project has both clients on the classpath, which happens more often than you would expect, your IDE will offer you the wrong import and the compiler will not stop you.
Pooling: JedisPooled Is Gone, Not Just Deprecated
The most-read question about Jedis pooling on Stack Overflow is titled "JedisPool vs JedisPooled". Jedis 8 answered it by deleting one of them.
| Class | 7.1.0 | 7.2.0 | 8.0.1 |
|---|---|---|---|
JedisPool | supported | @Deprecated | @Deprecated |
JedisPooled | supported | @Deprecated | removed |
JedisCluster | supported | @Deprecated | @Deprecated |
JedisSentinelPool | supported | @Deprecated | @Deprecated |
The deprecation notes all point the same way. JedisPool: "Use RedisClient instead. RedisClient provides the same functionality with a cleaner API and simplified constructor options." The wording is identical on JedisCluster and JedisSentinelPool, pointing at their own replacements.
Here is the migration in its usual shape. Before:
JedisPool pool = new JedisPool(poolConfig, "localhost", 6379);
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
}
pool.close();
After:
RedisClient client = RedisClient.builder()
.hostAndPort("localhost", 6379)
.poolConfig(poolConfig)
.build();
client.set("key", "value"); // no getResource(), no try-with-resources per call
client.close();
The borrow-and-return step disappears from your code. RedisClient takes a connection from the pool for the duration of each command and returns it, so the lifecycle you used to manage by hand is now the client's job. That is the whole point of the change, and it is why JedisPooled became redundant: JedisPooled existed to give you exactly this, and now the main class does it.
The pool itself is still Apache Commons Pool, and the defaults are still the ones Commons Pool has always had, with four overrides:
| Setting | Jedis default | What it does |
|---|---|---|
testWhileIdle | true | Idle connections are validated in the background |
minEvictableIdleTime | 60 s | How long a connection may sit idle before it is eligible for eviction |
timeBetweenEvictionRuns | 30 s | How often the evictor wakes up |
numTestsPerEvictionRun | -1 | Negative means test every idle connection on each run |
maxTotal | 8 | Commons Pool default, inherited. Almost always too low. |
ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
poolConfig.setMaxTotal(64);
poolConfig.setMaxIdle(16);
poolConfig.setMinIdle(4);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setMaxWait(Duration.ofMillis(500)); // fail fast instead of hanging
Size maxTotal against the number of threads that can issue a Redis command at once, not against your total thread count. Leaving it at 8 in a service with a 200-thread request pool is the most common Jedis production problem, and it presents as latency rather than as an error, which is why it takes so long to find.
Note also that commons-pool2 is a required dependency of Jedis and an optional one of Lettuce. That difference is the whole model. Jedis connections are not thread-safe, so the pool is not a tuning option here, it is the client. Lettuce and Redisson multiplex one connection across many threads, which is why pooling is something they can offer rather than something they need.
RESP3 Is On By Default in Jedis 8
This is the change most likely to surprise you during an upgrade, because nothing in your code has to change for it to take effect.
Jedis 7 and earlier skipped the HELLO handshake entirely and assumed RESP2 on the wire. Jedis 8 sets autoNegotiateProtocol to true by default, which means it sends HELLO 3 on connect and falls back to RESP2 if the server refuses. From the JedisClientConfig javadoc:
"When this returnstrueandgetRedisProtocol()isnull, the client sendsHELLO 3on connect and gracefully falls back to RESP2 if the server does not support RESP3. Whenfalseand the protocol isnull, the client preserves the legacy behaviour of skippingHELLOentirely and assuming RESP2 on the wire."
RESP3 is a better protocol. It carries types that RESP2 has to fake, so maps come back as maps instead of flattened arrays, doubles as doubles, and push messages arrive on their own channel instead of being multiplexed into replies. The catch is that your code may be reading the RESP2 shape. Commands whose replies change shape between the protocols include XPENDING, CONFIG GET, CLIENT INFO and the geo commands, and the ones that bite hardest are the ones you parse by index.
Three ways to control it, all on the config builder:
// Force RESP3, fail if unavailable
DefaultJedisClientConfig.builder().resp3().build();
// Force RESP2
DefaultJedisClientConfig.builder().resp2().build();
// Restore pre-8.0 behaviour: no HELLO at all, RESP2 assumed
DefaultJedisClientConfig.builder().serverDefaultProtocol().build();
If you set an explicit protocol, the auto-negotiation flag is ignored and the requested version is enforced strictly. serverDefaultProtocol() is the one to reach for if an upgrade breaks reply parsing and you need the old behaviour back while you fix it.
One footnote that catches people migrating gradually. The legacy Jedis class cannot speak the RESP3 wire format at all. It ignores the flag and logs a warning if you leave auto-negotiation enabled, so if your logs filled with protocol warnings after an upgrade, that is what they are.
TLS: Verification Is Now Strict
The second silent change. Jedis 8 defaults SslVerifyMode to FULL, which verifies the certificate chain against the truststore and checks that the hostname matches, by setting the endpoint identification algorithm to HTTPS.
| Mode | Chain verified | Hostname verified |
|---|---|---|
FULL (default) | yes | yes |
CA | yes | no |
INSECURE | no | no |
This breaks a specific and common deployment: reaching a managed Redis instance by IP address, or through a proxy or tunnel whose hostname does not appear in the certificate. It worked on Jedis 7 and it does not work on Jedis 8, and the failure is a handshake exception at connect time rather than anything that reads like a configuration problem.
SslOptions sslOptions = SslOptions.builder()
.truststore(new File("/etc/ssl/redis-truststore.jks"), "changeit".toCharArray())
.trustStoreType("JKS")
.sslVerifyMode(SslVerifyMode.CA) // chain yes, hostname no
.build();
RedisClient client = RedisClient.builder()
.hostAndPort("10.0.4.17", 6379)
.clientConfig(DefaultJedisClientConfig.builder()
.ssl(true)
.sslOptions(sslOptions)
.build())
.build();
CA is the honest setting for an IP-addressed endpoint with a certificate you trust. INSECURE disables both checks and should not reach production. The better fix, where you control the certificate, is a subject alternative name that matches what you connect to. Our TLS guide for Redis on Java covers the certificate side in more detail.
Timeouts
Jedis has one timeout constant, and it is used for two different things:
Protocol.DEFAULT_TIMEOUT = 2000; // milliseconds
That value is the default for both connectionTimeoutMillis and socketTimeoutMillis. Two seconds to establish a TCP connection, and two seconds of silence on the socket before a command gives up.
It is worth holding that next to the other Java client for a moment. Lettuce defaults its command timeout to 60 seconds. The same application, moved between the two clients with no other change, is thirty times less patient on Jedis. Neither default is wrong, and neither client's getting-started page mentions it. Jedis failing fast is usually the behaviour you want, but it means a Redis instance under load produces timeouts on Jedis long before it produces them on Lettuce, and teams running both read that as two different problems.
Then there is the third timeout, which defaults to zero:
DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(2000)
.socketTimeoutMillis(2000)
.blockingSocketTimeoutMillis(0) // the default: no socket timeout at all
.build();
Blocking commands need their own socket timeout, because a BLPOP that is legitimately waiting five seconds must not trip a two-second socket read. Jedis handles this by using a separate timeout for blocking commands, and by defaulting it to 0, which means no socket timeout at all.
In the normal case that is correct. In the case where the server goes away mid-block, or a firewall silently drops an idle connection, that thread waits indefinitely and the pooled connection never comes back. Set blockingSocketTimeoutMillis above the longest block you actually issue and the pool recovers instead of leaking:
.blockingSocketTimeoutMillis(35_000) // for BLPOP with a 30s timeout
Cluster and Sentinel on the New Classes
Both follow the same builder pattern. Cluster:
Set<HostAndPort> nodes = new HashSet<>(Arrays.asList(
new HostAndPort("node1", 7000),
new HostAndPort("node2", 7000),
new HostAndPort("node3", 7000)));
RedisClusterClient cluster = RedisClusterClient.builder()
.nodes(nodes)
.clientConfig(clientConfig)
.poolConfig(poolConfig)
.maxAttempts(5) // the default
.maxTotalRetriesDuration(Duration.ofSeconds(10))
.topologyRefreshPeriod(Duration.ofSeconds(30)) // off unless you set it
.build();
Two of those deserve attention. maxAttempts defaults to 5, which is how many times a command will be retried across nodes when it gets a MOVED or ASK redirect or a connection failure. maxTotalRetriesDuration bounds the wall-clock time those retries may consume, and without it five attempts against an unreachable node can take considerably longer than any timeout you set.
topologyRefreshPeriod is null by default, which means the cluster map is refreshed reactively, on redirects and errors, rather than on a schedule. That is adequate for most deployments and inadequate for the one case that matters: a planned failover where the client keeps sending to a node that no longer owns the slot until something errors. Setting a period costs one CLUSTER SLOTS call per interval. The cluster connection guide covers the topology side.
Sentinel is the same shape:
RedisSentinelClient sentinelClient = RedisSentinelClient.builder()
.masterName("mymaster")
.sentinels(sentinelNodes)
.clientConfig(clientConfig)
.sentinelClientConfig(sentinelConfig) // sentinels often have their own auth
.build();
The separate sentinelClientConfig matters more than it looks. Sentinel nodes frequently have different credentials from the data nodes, and the old JedisSentinelPool constructors made expressing that awkward enough that people gave up and matched the passwords.
MultiDbClient, and the Dependency Nobody Mentions
Jedis 7.0.0 added a client that almost nothing written about Jedis covers. MultiDbClient connects to several Redis deployments at once, weights them, health-checks them, and fails over between them with a circuit breaker. Its javadoc lists multi-endpoint support, automatic failover, the circuit breaker pattern, weight-based selection, health monitoring and retry logic.
MultiDbConfig config = MultiDbConfig.builder()
.database(new HostAndPort("redis-primary", 6379), 100.0f, clientConfig)
.database(new HostAndPort("redis-standby", 6379), 50.0f, clientConfig)
.failureDetector(MultiDbConfig.CircuitBreakerConfig.builder()
.failureRateThreshold(50.0f)
.slidingWindowSize(100)
.minNumOfFailures(10)
.build())
.commandRetry(MultiDbConfig.RetryConfig.builder()
.maxAttempts(3)
.exponentialBackoffMultiplier(2)
.build())
.retryOnFailover(true)
.build();
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(config)
.databaseSwitchListener(event -> log.warn("Redis failover to {}", event.getEndpoint()))
.build();
Note the shape. Retry and circuit-breaker settings live in their own nested config objects, MultiDbConfig.RetryConfig and MultiDbConfig.CircuitBreakerConfig, rather than as flat properties on the outer builder. Per-endpoint settings such as weight and healthCheckEnabled belong to DatabaseConfig, reachable through DatabaseConfig.builder(endpoint, clientConfig) when the three-argument shorthand above is not enough.
Highest weight wins while it is healthy. When the failure rate over the sliding window crosses the threshold, the circuit opens and traffic moves to the next endpoint. Health checks keep probing the failed one, and traffic returns when it recovers. The databaseSwitchListener is the hook you want wired to an alert, because a silent failover is a failover you find out about from a latency graph.
One thing to check before you write any of this. The classes behind MultiDbClient are built on resilience4j, and resilience4j is an optional dependency of Jedis. It is not on your classpath transitively. Add it explicitly:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
</dependency>
Without them you get a NoClassDefFoundError at the point the circuit breaker is first constructed, which is not where you will be looking. If you are building your own retry behaviour instead, exponential backoff and jitter in Java covers the arithmetic.
Pipelining and Transactions
Pipelining is where a blocking client earns most of its performance back. Each round trip costs you a network latency; batching a hundred commands into one flush costs you one. It is the clearest case in Redis work of trading latency for throughput, and on a blocking client the trade is heavily in your favour.
try (AbstractPipeline pipeline = client.pipelined()) {
Response<String> title = pipeline.get("book:978-0134685991");
pipeline.incr("views:978-0134685991");
pipeline.expire("views:978-0134685991", 3600);
pipeline.sync();
String value = title.get(); // only valid after sync()
}
The base classes changed name in 7.0.0. PipelineBase became AbstractPipeline and TransactionBase became AbstractTransaction, and pipelined() changed its return type. If you declared variables as PipelineBase, that is a compile error on the way to 8.
Transactions use the same shape, with MULTI and EXEC underneath:
try (AbstractTransaction tx = client.multi()) {
tx.set("account:1", "100");
tx.set("account:2", "200");
List<Object> results = tx.exec();
}
Redis transactions are not rollback transactions. EXEC runs the queued commands in order without interruption, and if one fails the others still apply. For anything that needs read-modify-write atomicity you want WATCH or a Lua script, both of which our transactions guide covers.
Thread Safety, and the Honest Comparison
A Jedis instance wraps one socket and is not thread-safe. Two threads sharing one will interleave their writes and read each other's replies, and the symptom is not an exception, it is a value from somebody else's command. That single fact explains the pool, it explains why JedisPooled existed, and it explains why RedisClient absorbed it.
The alternative design multiplexes: one connection, many threads, replies matched back to callers as they arrive. Lettuce works that way and Redisson does too, which is why neither needs a pool and Jedis does.
Neither approach is better in the abstract. Multiplexing gives you fewer sockets and no pool sizing, at the cost of a more complex client and a shared failure domain. Pooling gives you isolation between threads and a simpler mental model, at the cost of sizing a pool correctly and holding more connections open. Blocking commands are the clearest case where Jedis's model is the easier one: a BLPOP on a pooled connection is a thread waiting on a socket, which is exactly what it looks like.
If you are weighing the two properly rather than reading this after the decision, Jedis vs Lettuce puts them side by side.
Jedis in Spring Boot
Spring Boot ships Lettuce. Choosing Jedis means excluding one and adding the other:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
Spring Data Redis detects Jedis on the classpath and builds a JedisConnectionFactory. The properties it exposes are pool settings:
spring:
data:
redis:
host: redis.internal
port: 6379
timeout: 2s
jedis:
pool:
max-active: 64
max-idle: 16
min-idle: 4
max-wait: 500ms
Be aware of what this does not reach. Spring Data Redis builds on JedisConnectionFactory, which wraps the older classes rather than the RedisClient family, so the builder-based configuration in this guide is not available through Boot properties. Protocol selection, SslOptions and blockingSocketTimeoutMillis all need a JedisClientConfigurationBuilderCustomizer or a hand-built factory bean. If your Redis use in Spring is caching rather than direct commands, our Spring Boot caching guide covers that path instead.
When You Need More Than a Client
Everything above treats Redis as a command surface, which is what Jedis is for. Some requirements do not fit that shape, and you notice when you find yourself writing infrastructure rather than application code.
A distributed lock is the usual first one. Done properly it needs SET NX PX, a unique owner token, a Lua script for the release so you cannot unlock somebody else's lock, and a watchdog that renews the lease while work is still running. That is a few hundred lines you now maintain. A near cache is the second: a local map, a pub/sub invalidation listener, and a consistency story for the window between write and invalidate.
This is where a higher-level client like Redisson fits. It is not a faster Jedis; it is a different layer, exposing Java types backed by Redis rather than Redis commands:
RLock lock = redisson.getLock("order:4711");
lock.lock(); // lease renewal handled for you
try {
// ...
} finally {
lock.unlock();
}
RMapCache<String, Book> books = redisson.getMapCache("books");
books.put("978-0134685991", book, 30, TimeUnit.MINUTES, 10, TimeUnit.MINUTES);
A near cache with invalidation becomes configuration, and its local eviction can be backed by Caffeine:
LocalCachedMapOptions<String, Book> options = LocalCachedMapOptions.<String, Book>name("books")
.cacheProvider(CacheProvider.CAFFEINE)
.syncStrategy(SyncStrategy.INVALIDATE) // the default
.cacheSize(10_000)
.timeToLive(Duration.ofMinutes(30));
RLocalCachedMap<String, Book> books = redisson.getLocalCachedMap(options);
RLock, RMapCache, RLocalCachedMap and RedissonSpringCacheManager are all open-source under Apache 2.0. Putting a near cache behind @Cacheable, which is RedissonSpringLocalCachedCacheManager, requires Redisson PRO, as do the clustered and V2 cache managers.
Redisson is also a heavier dependency that holds its own connections and wants its own tuning. If your application needs GET, SET and a sorted set, staying on Jedis is the right call and adding a second client is not. The timing argument is the only one worth making: if the 8.0 upgrade means rewriting your connection layer anyway, that is the cheapest moment you will get to compare. Redisson vs Jedis puts them side by side, and the migration guide covers what changes if you move.
Frequently Asked Questions
What Is Jedis?
Jedis is a Java client for Redis and Valkey. It exposes Redis commands as Java methods over a blocking socket connection, and because a single connection is not thread-safe it uses a connection pool. Since version 7.2.0 the recommended entry points are RedisClient, RedisClusterClient and RedisSentinelClient, which replace the older JedisPool, JedisCluster and JedisSentinelPool classes.
What Is the Difference Between Redis and Jedis?
Redis is the server: an in-memory data store that holds your keys and executes commands. Jedis is one of several Java client libraries that talk to it over the network. Redis is what runs on port 6379; Jedis is the dependency in your POM that lets Java code send it commands. The other common Java clients are Lettuce and Redisson.
Is There a Redis Library for Java?
There are three in common use. Jedis is small and blocking, and pools connections. Lettuce is built on Netty, multiplexes one thread-safe connection, and is the client Spring Boot uses by default. Redisson exposes Java data structures such as Map, Lock and Queue backed by Redis rather than exposing Redis commands directly. All three work against Redis and Valkey.
Is JedisPool Deprecated?
Yes. JedisPool, JedisCluster and JedisSentinelPool were marked @Deprecated in Jedis 7.2.0, with the javadoc directing users to RedisClient, RedisClusterClient and RedisSentinelClient. JedisPooled went further and was removed entirely in Jedis 8.0.0, so code using it will not compile against 8.x. The deprecated classes still work, but new code should use the RedisClient family.
Why Did My Jedis Upgrade Break TLS or Change Reply Types?
Two defaults changed in Jedis 8.0.0. TLS verification now defaults to SslVerifyMode.FULL, which checks the hostname as well as the certificate chain, so connecting to a managed instance by IP address fails where it previously succeeded; SslVerifyMode.CA restores the old behaviour. Separately, the client now negotiates RESP3 by default, which changes the shape of some replies. DefaultJedisClientConfig.builder().serverDefaultProtocol() restores the pre-8.0 RESP2 behaviour.
Next Steps
A Jedis 8 client on the RedisClient family, with a pool sized against real concurrency, a blocking socket timeout that is not zero, a deliberate choice about RESP3, and TLS verification you understand rather than inherit, will behave predictably in production. None of that is the default, and none of it is in the getting-started material.
From here, connecting to Redis in Java covers the ground across clients, and Redis Cluster and Redis Sentinel cover the topologies this configuration protects you against.
Jedis itself deals in strings and bytes, so serialization is your problem rather than the client's. Our guide to serialization codecs covers what happens to your objects on the way to the wire.
If distributed locks, collections or a near cache are on your list, try Redisson PRO free or compare the editions in the feature comparison.