How to Use Redis (or Valkey) in Java: A Complete Guide with Code Examples
Redis is one of the most widely used in-memory data stores, and Java is one of the most widely used languages to build the services that rely on it. The catch: Java can't talk to Redis on its own. Redis speaks its own wire protocol over a TCP socket, so you need a client library to bridge the two.
This guide walks you through everything you need to go from an empty project to a running Java application backed by Redis (or Valkey, the open-source fork that speaks the same protocol). We'll use Redisson, a high-level Redis and Valkey client that exposes familiar Java types, like Map, List, and Lock, backed by the server instead of local memory.
By the end you'll have working code for the core data structures and a clear map of where to go next for clustering, caching, locking, and production hardening.
Which Redis Client Should You Use in Java?
There are three clients most Java teams consider, and they sit at different levels of abstraction:
- Jedis — a thin, synchronous wrapper around Redis commands. You send commands, you parse replies. Simple, but you build everything above it yourself.
- Lettuce — a lower-level client too, with strong asynchronous and reactive support built on Netty.
-
Redisson — a higher-level client. Instead of raw commands, you work with distributed implementations of standard Java interfaces (
Map,Set,Queue,Lock,AtomicLong) plus services like caching, pub/sub, and rate limiting.
If you want raw command access, Jedis or Lettuce are fine. If you'd rather write ordinary Java and let the library handle the distributed plumbing, Redisson is the faster path, and it's what this guide uses. For a side-by-side breakdown, see Redisson vs Jedis, Redisson vs Lettuce, and Redisson vs Spring Data Redis.
Prerequisites
Before you start, make sure you have:
-
JDK 8 or later installed (
java -version). - Maven or Gradle for dependency management.
- A running Redis or Valkey server. For local development you can run one in seconds. If you don't have one yet, follow How to install Redis, or start a container:
# Redis
docker run -d --name redis -p 6379:6379 redis
# ...or Valkey (same protocol, same code)
docker run -d --name valkey -p 6379:6379 valkey/valkey
Confirm it responds with redis-cli:
redis-cli ping
# PONG
Everything below works identically against Valkey, since Valkey is wire-compatible with Redis. If you're weighing the two, see Valkey vs Redis: A Comparison.
Step 1 — Add the Redisson Dependency
Add Redisson to your build. Use the latest release from Maven Central — 4.6.1 at the time of writing. Redisson 4.x runs on JDK 8 and later and supports Redis 3.0+ and Valkey 7.2.5+.
Maven (pom.xml):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>4.7.0</version>
</dependency>
Gradle (build.gradle):
implementation 'org.redisson:redisson:4.7.0'
Step 2 — Connect to Redis or Valkey
Redisson is configured through a Config object and started with Redisson.create(). The most common setup, a single server, looks like this:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class Application {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// ... use the client ...
redisson.shutdown();
}
}
A few things worth knowing:
- The
redis://scheme is used for both Redis and Valkey. For a TLS connection, userediss://(see the note on production below). - If your server requires authentication, add
.setPassword("your-password")to theuseSingleServer()builder. -
RedissonClientis thread-safe and manages its own connection pool. Create one instance per application and reuse it, don't build a new client per request.
That single-server config is one of several topologies. Redisson also supports Redis Cluster, Sentinel, replicated, and managed cloud deployments, and the only thing that changes is the config block. See the configuration docs for the full list.
Step 3 — Your First Operations
With a client in hand, the simplest thing you can store is a single object. Redisson calls this an RBucket:
import org.redisson.api.RBucket;
RBucket<String> bucket = redisson.getBucket("greeting");
bucket.set("Hello from Redisson");
String value = bucket.get();
System.out.println(value); // Hello from Redisson
The other structure you'll reach for constantly is a map. RMap implements java.util.concurrent.ConcurrentMap, so it behaves like a Map you already know, except the data lives on the server and is visible to every instance of your application:
import org.redisson.api.RMap;
RMap<String, String> map = redisson.getMap("user:1001");
map.put("name", "Ada");
map.put("role", "engineer");
String name = map.get("name"); // Ada
boolean isEngineer = map.containsKey("role");
Under the hood an RMap is stored as a Redis hash; Redisson just gives you the Java Map API on top of it.
Step 4 — Common Data Structures
Redisson maps most of the standard Java collections onto Redis data types. Here are the ones you'll use most often.
Lists — an ordered, index-addressable collection (RList implements java.util.List):
import org.redisson.api.RList;
RList<String> list = redisson.getList("tasks");
list.add("compile");
list.add("test");
list.add("deploy");
System.out.println(list.size()); // 3
System.out.println(list.get(0)); // compile
Sets — unique elements, no duplicates (RSet implements java.util.Set):
import org.redisson.api.RSet;
RSet<String> tags = redisson.getSet("post:42:tags");
tags.add("redis");
tags.add("java");
tags.add("redis"); // ignored, already present
Queues — FIFO access, useful for lightweight job passing (RQueue implements java.util.Queue):
import org.redisson.api.RQueue;
RQueue<String> queue = redisson.getQueue("jobs");
queue.add("job-1");
String next = queue.poll(); // job-1
Atomic counters — a distributed AtomicLong, safe to increment from many machines at once:
import org.redisson.api.RAtomicLong;
RAtomicLong counter = redisson.getAtomicLong("page:views");
counter.incrementAndGet();
counter.addAndGet(10);
long total = counter.get();
These are the everyday building blocks. For a deeper tour of the most useful objects and when to reach for each, see The Top 5 Redis-Based Java Objects. If you need to store structured records rather than flat values, storing JSON with Redis on Java covers that pattern.
Step 5 — Beyond the Basics
Once the collections feel natural, Redisson's higher-level services are where it pulls ahead of a raw command client. A few you'll likely want:
-
Distributed locks. An
RLockimplementsjava.util.concurrent.locks.Lockbut coordinates across every node in your system, so only one instance runs a critical section at a time:
Full patterns, including lease times and fair locks, are in How to Use Redis Locks in Java.import org.redisson.api.RLock; RLock lock = redisson.getLock("order:1001"); lock.lock(); try { // only one node executes this at a time } finally { lock.unlock(); } - Caching. Redisson can back a read-through/write-through cache so hot data stays close to your application. See How to Use Redis Cache in Java.
- Publish/subscribe messaging. Decouple producers and consumers with topics: How to Use Redis Pub/Sub in Java.
- Rate limiting. Throttle traffic across a fleet with a distributed limiter: Distributed Rate Limiting in Java with Spring Boot.
- Streams. For append-only logs and event processing, see Redis Streams for Java.
Redisson also offers synchronous, asynchronous, reactive, and RxJava APIs for nearly every object, so you can match it to whatever concurrency model your service already uses.
Build and Run the App
Putting it together, here's a complete, runnable example:
import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class Application {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RBucket<String> bucket = redisson.getBucket("greeting");
bucket.set("Hello from Redisson");
System.out.println("bucket: " + bucket.get());
RMap<String, String> map = redisson.getMap("user:1001");
map.put("name", "Ada");
System.out.println("map: " + map.get("name"));
redisson.shutdown();
}
}
Save this as src/main/java/Application.java, in a project whose pom.xml contains the Redisson dependency from Step 1. Then compile and run it with Maven:
mvn compile
mvn exec:java -Dexec.mainClass=Application
You should see the stored values printed back. That's a working Redis-backed Java application.
Production Notes
The single-server setup above is perfect for local development. Before you ship, a few things change:
-
Security. Redis and Valkey ship with no authentication by default, which is dangerous if the port is reachable. Require a password (
.setPassword(...)) and use TLS with therediss://scheme. Never expose the server directly to the internet. On Redis 6 and later a password alone is often not enough — ACL users need.setUsername(...)too, and credentials embedded in the address are ignored, both of which surface as NOAUTH Authentication required. - High availability. Production usually means a cluster or Sentinel topology rather than a single node. Redisson handles failover and node discovery for you once configured.
- Managed services. If you run on a cloud provider, Redisson connects to the managed offerings directly, for example AWS ElastiCache, Azure Cache, Google Cloud Memorystore, and Amazon MemoryDB. See managed Redis for the disabled commands and clustering differences these services introduce.
-
Connection reuse. As noted, keep one
RedissonClientfor the lifetime of the app. How many sockets that client opens, and what happens when your fleet outgrows the server's limit, is covered in connection management at scale below.
For workloads that need local (near) caching, data partitioning, or advanced cache strategies, Redisson PRO adds those on top of the same API. You can try it for free.
Connection Management at Scale
The defaults above are tuned for a single application talking to a single server, and they work without thought until the day they don't. Two things change once you run more than one instance: how many sockets your fleet actually opens, and what happens when the server refuses the next one. Both are worth understanding before you meet them during an incident.
Pooling, Multiplexing, and Why Redisson Uses Both
Java clients manage connections in one of two ways, and the choice shapes everything above it.
-
Connection pooling. The client keeps a set of open connections. A thread borrows one, sends a command, waits for the reply, and returns it. One command occupies one connection for a full round trip, so throughput is bounded by pool size, and a thread that finds the pool empty blocks until someone gives a connection back. This is how Jedis works, which is why a raw
Jedisinstance is not thread-safe and whyJedisPoolexists. - Multiplexing. The client keeps one connection open and writes commands from many threads onto it, reading replies as they come back. Because Redis processes commands on a single thread and answers in order, one socket can carry thousands of concurrent callers without any of them waiting on each other. Lettuce and Redisson both work this way, on Netty.
Redisson does both: it holds a pool of multiplexed connections. That combination looks redundant until you hit the reason for it — a blocking command occupies its connection for as long as it blocks. A BLPOP that waits thirty seconds for an element would stall every other caller sharing that socket, so blocking operations need a connection to themselves. That is precisely why RBlockingQueue, RLock and the pub/sub listeners can wait without freezing the rest of your application: they take a connection out of the pool, while ordinary commands continue to share the multiplexed ones. Redis's own documentation on pools and multiplexing names this as multiplexing's one hard limitation.
For the wider comparison of how each Java client handles this, see what a Redis Java client is.
How Many Connections Redisson Actually Opens
More than most people expect, and the setting names differ by topology. In single-server mode:
-
connectionPoolSize— 64, the maximum. -
connectionMinimumIdleSize— 24, held open even when idle.
In cluster, replicated, sentinel and master/slave modes those are replaced by per-node settings, and this is the part that surprises people:
-
masterConnectionPoolSize64 andmasterConnectionMinimumIdleSize24 — per master node. -
slaveConnectionPoolSize64 andslaveConnectionMinimumIdleSize24 — per replica. These apply even if you never issue a read yourself, because the defaultreadModeisSLAVE.
Pub/sub is pooled separately again, through subscriptionConnectionPoolSize (50), subscriptionConnectionMinimumIdleSize (1) and subscriptionsPerConnection (5) — so a service with more than five channels opens a second subscription connection, and so on.
maxclients, and the Arithmetic That Catches People
Work an example. A three-master, three-replica cluster, twenty application instances, everything left at defaults:
idle per instance = (24 x 3 masters) + (24 x 3 replicas) = 144
across the fleet = 144 x 20 instances = 2,880 connections
ceiling per instance = (64 x 3) + (64 x 3) = 384
across the fleet = 384 x 20 = 7,680 connections
...plus subscription connections
That is 2,880 sockets open before a single request arrives, and a ceiling near 7,680 under load. Redis defaults maxclients to 10,000 — and lowers it silently if the process's file-descriptor soft limit won't stretch that far, since it reserves 32 descriptors for itself. Cross the line and new connections are refused with ERR max number of clients reached, which surfaces in Java as a connection failure at startup or during a scale-out, not as a slow query.
This is why connection problems tend to appear the moment you double your instance count, or during a rolling deploy when old and new pods briefly overlap. Managed services make it tighter: several cloud providers cap connections well below 10,000 on smaller tiers, so check the ceiling for your plan — see managed Redis for the limits and command restrictions these services introduce. If you run on Kubernetes, Redis and Valkey on Kubernetes covers pod churn and connection storms specifically.
The fix is usually not a bigger server. Lower connectionMinimumIdleSize (or the per-node equivalents) so instances stop holding sockets they aren't using, and size the maximum against real concurrency rather than leaving it at 64 per node.
Timeouts, Retries, and Keepalive
The remaining defaults decide how failures present themselves:
-
timeout— 3000 ms to wait for a response once a command is sent. -
connectTimeout— 10000 ms to establish a connection. -
idleConnectionTimeout— 10000 ms before an unused connection is closed. -
retryAttempts— 4 retries after the first send, so five attempts in total before the command fails. -
pingConnectionInterval— 30000 ms between keepalive pings, which is what detects connections a firewall or load balancer has silently dropped.
When RedisTimeoutException appears, the pool is often not the cause. Redisson's own guidance is to raise nettyThreads first (32, 64, 128, 256), then the timeout, and only then the pool size — because a busy Netty event loop delays both sending commands and decoding replies, and looks identical to a slow server from the application's side. Blocking code inside a listener produces the same symptom.
None of this is visible from the server. Redis reports a healthy connected_clients while your threads queue for a connection inside the JVM, so pool state has to be measured client-side — see Redis monitoring for what the server cannot tell you. The full parameter list lives in the Redisson configuration reference, and every other default that behaves this way is collected in one place.
Frequently Asked Questions
Can Java Connect to Redis Directly?
No. Redis has no native Java API, so you need a client library, such as Redisson, Jedis, or Lettuce, that speaks the Redis protocol over the network.
What Is the Best Redis Client for Java?
It depends on the abstraction you want. Jedis and Lettuce give you low-level command access; Redisson gives you distributed versions of standard Java types plus higher-level services like locks, caching, and rate limiting. Teams that prefer writing ordinary Java tend to choose Redisson.
Does Redisson Work With Valkey?
Yes. Valkey is wire-compatible with Redis, so the same configuration, addresses, and code shown here work against Valkey without changes.
How Do I Connect to a Redis Cluster in Java?
Swap the single-server config for useClusterServers() and add your node addresses. See How to Connect to Redis Cluster in Java for the full setup.
Do I Need to Create a New Client for Every Request?
No. RedissonClient is thread-safe and pools connections internally. Create one instance at startup, share it across your application, and shut it down when the app stops.
Why Am I Seeing "max number of clients reached"?
Your fleet has crossed the server's maxclients limit. It defaults to 10,000, drops automatically if the process's file-descriptor limit is lower, and is often capped well below that on managed plans. This is usually fleet arithmetic rather than a traffic spike: lower connectionMinimumIdleSize (or the per-node equivalents) so instances stop holding sockets they aren't using, then size the maximum against real concurrency.
Next Steps
You now have a Redis- or Valkey-backed Java app and the core data structures to build on. From here, pick the capability your use case needs, locks, caching, pub/sub, or clustering, and go deeper. For a tour of what teams actually build on it, see Redis use cases and how to build them in Java. The full Redisson documentation covers every object and service in detail.