AZ Affinity in Java: Same-Zone Valkey Replica Reads With Redisson
Every replicated Redisson topology reads from replicas by default. readMode is SLAVE, and the default load balancer rotates reads across all of a shard's replicas without regard to where they run. Spread a shard over three availability zones in the usual way, one primary and two replicas, run clients in all three, and two thirds of your reads leave the zone they started in. Each crossing adds round-trip latency, and on most clouds it adds a line to the bill.
Valkey 8.0 gave each node a way to say which zone it runs in. Redisson 4.8.0, released on 2 October 2026, uses it: three new read modes send reads to nodes in the client's own zone first and cross zones only when they have to. This guide covers what the modes do, the arithmetic that decides which one fits your layout, the failure behaviour that changes once reads stop being spread out, and the setup on ElastiCache, on Kubernetes and on your own nodes. Redisson is our product; where another client does the same thing, this guide says so.
What AZ Affinity Is, and What the Server Does
AZ affinity is a client-side read strategy, and the server's part is small. From Valkey 8.0, each node accepts an availability-zone setting and reports it in the availability_zone field of INFO SERVER. Valkey 8.1 added the field to the HELLO reply, and Valkey 9.1 added it, spelled availability-zone, to CLUSTER SHARDS and CLUSTER SLOTS. These replies include it only when a zone is set. The shipped valkey.conf describes the value as "exposed in the INFO and HELLO commands for clients to use."
valkey-cli -h 10.0.2.17 INFO SERVER | grep availability_zone
availability_zone:us-east-1b
That is all the server does with it. Valkey does not route, redirect or reorder anything by zone, so the feature helps only if your client implements it.
Redis has no equivalent setting. A Redis server never reports a zone, so a zone-aware client treats every Redis node as remote and nothing changes. The same applies to Valkey 7.2, which predates the setting. Everything below assumes Valkey 8.0 or later.
What a Cross-Zone Read Costs
Zones in one region are separate facilities with their own power and network, up to about 100 km apart on AWS. The Valkey project's own write-up of the feature puts the extra round trip at roughly 500 to 1,000 microseconds. In its worked example, average read latency fell from about 800 µs with plain replica reads to about 300 µs once reads stayed in the zone. Half a millisecond looks like nothing until a request makes twenty sequential cache reads, at which point it is ten milliseconds the request never needed.
The money depends on who bills the crossing, and the providers do not agree:
| Platform | Cross-zone read traffic | Billed to |
|---|---|---|
| ElastiCache, node-based | $0.01 per GB | Your application's side only. AWS does not bill the ElastiCache node itself for cross-AZ transfer |
| Valkey you run on EC2 | $0.01 per GB in each direction | Both instances: the replica that sends and the client that receives |
| Memorystore for Valkey | $0.01 per GiB processed for clients in another zone | Your project, through the Private Service Connect endpoint |
| Azure | No fee; Microsoft dropped inter-zone transfer charges in 2024 | Nobody. Latency is the only reason to care |
| ElastiCache Serverless | No cross-AZ charge | Nobody. The service routes replica reads to the client's zone itself when it can |
Rates are as each provider published them in October 2026. The table is also a scope statement: the setting in this guide is for node-based clusters and self-managed nodes, where you choose which replica serves a read.
The Layout Sets the Floor
Before you choose a mode, count where your nodes are. A read can stay in its zone only if that zone holds a node of the shard being read, and no read mode changes that. The table works through the two layouts most deployments use, with clients spread evenly over three zones. Each figure is the share of reads that crosses a zone boundary. Cluster mode gives the same result, because every shard repeats the layout with its primary in a different zone.
| Read mode | Primary in A, replicas in B and C | Primary in A, replica in B |
|---|---|---|
SLAVE (default) | 67% | 67% |
MASTER | 67% | 67% |
MASTER_SLAVE | 67% | 67% |
AZ_AFFINITY | 33% | 67% |
AZ_AFFINITY_SLAVES_AND_MASTER | 0% | 33% |
AZ_AFFINITY_MASTER_SLAVE | 0% | 33% |
Three things follow. The default and its two siblings all send two thirds of reads across zones, so spreading reads evenly does nothing for locality. With one replica per shard, AZ_AFFINITY changes nothing, because every client outside that replica's zone has to cross to reach it. And zero is reachable only when every client zone holds a node of every shard, and, where that node is the primary, the mode lets the primary serve reads.
Put numbers on the first layout. Say a service reads 50 MB/s of cache responses on average, about 10,000 reads a second at 5 KB each. Over a 730-hour month that is about 131,400 GB of responses. On node-based ElastiCache, where your side pays $0.01 per GB:
| Read mode | Share crossing zones | Cross-zone GB per month | Transfer cost per month |
|---|---|---|---|
SLAVE (default) | 67% | ~87,600 | ~$876 |
AZ_AFFINITY | 33% | ~43,800 | ~$438 |
AZ_AFFINITY_SLAVES_AND_MASTER | 0% | ~0 | ~$0 |
Self-managed Valkey on EC2 is billed on both ends, which doubles the last column. Requests add a little on top, but for reads the responses dominate.
The Three AZ Read Modes
Each mode is an ordered list of steps. Redisson groups the shard's usable nodes into those steps, takes the first step that has at least one node, and applies the configured loadBalancer (RoundRobinLoadBalancer by default) only inside that step. Nodes that are disconnected or marked as failed are removed before the grouping, which is how fallback happens.
| Read mode | Order of steps | Valkey GLIDE name |
|---|---|---|
AZ_AFFINITY | Replicas in the client's zone, then replicas in any zone, then the master | AZ_AFFINITY |
AZ_AFFINITY_SLAVES_AND_MASTER | Replicas in the zone, then the master if it is in the zone, then replicas in any zone, then the master | AZ_AFFINITY_REPLICAS_AND_PRIMARY |
AZ_AFFINITY_MASTER_SLAVE | The master and replicas in the zone with no preference between them, then all nodes | AZ_AFFINITY_ALL_NODES |
AZ_AFFINITY keeps reads off the primaries while any replica is available. Choose it when the primaries are busy with writes, or when every zone holds a replica of every shard. The price shows in the layout table: in the common three-node layout, clients in each primary's zone still cross for every read of that shard.
AZ_AFFINITY_SLAVES_AND_MASTER reaches zero in that layout. Clients in the primary's zone read from the primary, so each primary takes one zone's reads on top of all the writes. Size primaries for that before you switch. Do not treat those primary reads as a consistency feature, though. Clients in the other zones still read replicas, so the same code sees different staleness depending on where it runs.
AZ_AFFINITY_MASTER_SLAVE drops the preference between node types. Where a zone holds both the primary and a replica of a shard, it spreads that zone's reads over both. Where the client's zone holds no node of the shard, it spreads the fallback over every node, primary included, instead of trying remote replicas first. In the one-node-per-zone layout it crosses zones exactly as often as the previous mode. Both of these modes, like MASTER_SLAVE, also put the master into the pool that subscriptionMode SLAVE draws from.
Configuring Redisson
Two settings do it: the read mode, and the zone the client runs in. Both sit on the topology you already configure. Add the dependency at version 4.8.0 or later:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>4.8.0</version>
</dependency>
Config config = new Config();
config.useClusterServers()
.addNodeAddress("valkeys://clustercfg.orders-cache.xxxxxx.use1.cache.amazonaws.com:6379")
.setReadMode(ReadMode.AZ_AFFINITY_SLAVES_AND_MASTER)
.setClientAvailabilityZone(System.getenv("CLIENT_AZ")); // e.g. "us-east-1a"
RedissonClient redisson = Redisson.create(config);
If CLIENT_AZ is missing, that code fails at startup: an AZ read mode without clientAvailabilityZone throws IllegalArgumentException, and so does a blank zone. That is the behaviour you want. The YAML equivalent uses Redisson's ${VAR} placeholders, which resolve from environment variables and system properties:
clusterServersConfig:
nodeAddresses:
- "valkeys://clustercfg.orders-cache.xxxxxx.use1.cache.amazonaws.com:6379"
readMode: "AZ_AFFINITY_SLAVES_AND_MASTER"
clientAvailabilityZone: "${CLIENT_AZ}"
The YAML form has one trap. A placeholder that resolves to nothing and has no :-default stays in the file as the literal text ${CLIENT_AZ}. That is a valid zone name that matches no node, so every read quietly falls through to the cross-zone steps and nothing is logged. Make sure the variable is set wherever the file is used, or build the config in code. In Spring Boot, the Redisson starter reads the same file through spring.redis.redisson.file.
The same two settings work in every topology that has replicas to choose between. That means useClusterServers() (ElastiCache with cluster mode enabled), useReplicatedServers() (ElastiCache with cluster mode disabled), useSentinelServers() for Sentinel, useMasterSlaveServers(), and, in Redisson PRO, the Multi Cluster and Multi Sentinel modes used for cross-region replicas. Single-server and proxy modes have no replicas to choose from. The managed Redis guide maps each ElastiCache, Azure and Memorystore deployment type to its topology, and the full option list is in the configuration reference.
Finding the Client's Zone
The application has to know its own zone. Where to get it depends on where it runs:
- EC2, and ECS on EC2: the instance metadata service returns the zone name at
placement/availability-zoneand the zone ID atplacement/availability-zone-id. - ECS on Fargate: the task metadata endpoint,
${ECS_CONTAINER_METADATA_URI_V4}/task, includes anAvailabilityZonefield on platform version 1.4 and later. - Kubernetes: the zone is the node's
topology.kubernetes.io/zonelabel. Pods cannot read node labels directly. Newer releases can copy the topology labels onto each Pod through thePodTopologyLabelsadmission plugin (alpha in 1.33, beta with its feature gate on by default from 1.35), after which the Downward API exposes the zone as an environment variable. Without that, inject the zone from your deployment tooling or read it from the cloud metadata service.
# Pod spec: Kubernetes 1.35+, or 1.33-1.34 with PodTopologyLabelsAdmission enabled
env:
- name: CLIENT_AZ
valueFrom:
fieldRef:
fieldPath: metadata.labels['topology.kubernetes.io/zone']
Match the format the nodes report. Redisson compares zone names exactly. On AWS a zone name such as us-east-1a is an alias that, in some regions, maps to different physical zones in different accounts, while a zone ID such as use1-az1 means the same place everywhere. If your cache and your application live in different accounts, prefer IDs if the nodes report them, and in every case run INFO SERVER against one node and copy what you see.
Redisson does not read zones from CLUSTER SHARDS or HELLO. It sends INFO SERVER to each node and parses the availability_zone line, which works from Valkey 8.0 in every topology. It does this whenever it sets a node up: at startup, for a replica added later, for a new master after a failover, and when a disconnected node comes back.
Setting the Zone on Nodes You Run Yourself
Managed services such as ElastiCache set each node's zone for you. On your own nodes, set it once per node and persist it:
# at runtime
valkey-cli -h 10.0.1.12 CONFIG SET availability-zone us-east-1a
# valkey.conf, so it survives a restart
availability-zone "us-east-1a"
On Kubernetes, pass the same zone label to each Valkey Pod and write it into the config at start-up. Our guide to running a Valkey cluster on Kubernetes covers the StatefulSet.
Per-Object Read Modes
Since Redisson 4.5.0 a single object can override the global read mode. That retires the old workaround of running two clients, one for reads that must see their own writes and one for everything else, described under replication and stale reads. Typical use: zone-local replica reads globally, and the master for the few objects read back right after a write.
import org.redisson.api.RBucket;
import org.redisson.api.RMap;
import org.redisson.api.options.MapOptions;
import org.redisson.api.options.PlainOptions;
import org.redisson.config.ReadMode;
// Global readMode is AZ_AFFINITY_SLAVES_AND_MASTER.
// A session is read back straight after it is written, so read it from the master.
RMap<String, String> session = redisson.getMap(
MapOptions.<String, String>name("session:" + sessionId).readMode(ReadMode.MASTER));
// Inherits the global mode: zone-local reads.
RBucket<Product> product = redisson.getBucket(PlainOptions.name("product:" + sku));
The override is available on PlainOptions, MapOptions, MapCacheOptions and LocalCachedMapOptions. It can only choose among nodes the global configuration has prepared, which gives three rules worth knowing:
| Override | Works when | Otherwise |
|---|---|---|
MASTER | Always | Nothing else to know |
| Any of the three AZ modes | clientAvailabilityZone is set, and the global readMode and subscriptionMode are not both MASTER | Without a zone, creating the object throws. With both global settings on MASTER, Redisson never builds the replica pool and the override has no effect |
AZ_AFFINITY_SLAVES_AND_MASTER or AZ_AFFINITY_MASTER_SLAVE | The global mode puts the master in the read pool: MASTER_SLAVE or one of these two | Under any other global mode (SLAVE, AZ_AFFINITY, or MASTER with subscriptionMode SLAVE), behaves like AZ_AFFINITY |
So choose the global mode for the master question first, then override per object. If any object should be allowed to read a primary in its own zone, the global mode has to be one that includes the master.
Verifying It Works
Start with the nodes. Run INFO SERVER against each one and confirm availability_zone is set and spelled the way your client expects. Then turn on two DEBUG loggers for a short while:
<!-- logback.xml -->
<logger name="org.redisson.connection.ClientConnectionsEntry" level="DEBUG"/>
<logger name="org.redisson.connection.pool.SlaveConnectionPool" level="DEBUG"/>
The first prints one line per node with the zone it reported, and logs a warning for any node that reported none. The second prints the step each read was served from. Step 0 is always a node in the client's zone, and under AZ_AFFINITY_SLAVES_AND_MASTER step 1 is too: it is the master in your zone, so clients there will log step 1 for reads that never left it. It logs every read, so do not leave it on in production. After that, compare command counts per replica: replicas should now take reads in proportion to the clients in their zone rather than evenly. Redis client metrics in Java covers exporting the client-side view.
Coming From GLIDE
From Valkey GLIDE. GLIDE implemented AZ affinity first, in version 1.2 in November 2024, and the Valkey project's original write-up of the feature is about it. The concepts map closely, though two of GLIDE's values are recent: ALL_NODES needs GLIDE 2.4 and AZ_AFFINITY_ALL_NODES needs Java GLIDE 2.5.3.
| Valkey GLIDE | Redisson |
|---|---|
ReadFrom.PRIMARY (GLIDE's default) | ReadMode.MASTER |
ReadFrom.PREFER_REPLICA | ReadMode.SLAVE (Redisson's default) |
ReadFrom.ALL_NODES | ReadMode.MASTER_SLAVE |
ReadFrom.AZ_AFFINITY | ReadMode.AZ_AFFINITY |
ReadFrom.AZ_AFFINITY_REPLICAS_AND_PRIMARY | ReadMode.AZ_AFFINITY_SLAVES_AND_MASTER |
ReadFrom.AZ_AFFINITY_ALL_NODES | ReadMode.AZ_AFFINITY_MASTER_SLAVE |
.clientAZ("us-east-1a") | .setClientAvailabilityZone("us-east-1a") |
Note the defaults in the first two rows. GLIDE reads from the primary unless told otherwise; Redisson reads from replicas. A migration that carries over no read setting changes your consistency behaviour. The rest of the move is covered in the Valkey GLIDE to Redisson migration guide.
Frequently Asked Questions
What Is AZ Affinity in Valkey?
It is a client read strategy that sends reads to replicas in the application's own availability zone and falls back to other zones only when no suitable local node is available. Valkey 8.0 and later lets each node report its zone through the availability-zone setting, and the client uses that to choose a node. The server does no zone-based routing itself.
Does Redis Support Availability Zone Affinity?
Not on the server side. Redis has no availability-zone setting and reports no zone, so zone-aware clients treat every Redis node as remote. You can still keep reads local with a client that filters nodes by address, but you then maintain the zone mapping in your own configuration.
Which Java Clients Support AZ Affinity?
Among the widely used Java clients, two as of October 2026. Valkey GLIDE added it in version 1.2, and Redisson added it in 4.8.0 with the AZ_AFFINITY, AZ_AFFINITY_SLAVES_AND_MASTER and AZ_AFFINITY_MASTER_SLAVE read modes. Redisson supports them in Cluster, Replicated, Sentinel and master/slave topologies. Lettuce and Jedis have no zone-aware read option.
Does AZ Affinity Reduce ElastiCache Costs?
On node-based clusters, yes. AWS bills cross-AZ traffic between your application and ElastiCache at $0.01 per GB on the application side, so every read that stays in the zone avoids that charge. The saving depends on the layout: with one primary and two replicas over three zones, the default sends about two thirds of reads across zones, AZ_AFFINITY about a third, and AZ_AFFINITY_SLAVES_AND_MASTER almost none. ElastiCache Serverless has no cross-AZ charge and routes replica reads to the client's zone on its own.
What Happens If There Is No Replica in My Zone?
The read falls back to the next step in the mode's order, so it is served from another zone rather than failing. With AZ_AFFINITY it goes to replicas in other zones and then to the primary. With AZ_AFFINITY_SLAVES_AND_MASTER, a primary in your zone is used before any remote replica. A read fails only if no node in the shard can serve it at all.
Next Steps
Start by counting which zones your clients run in and which zones hold each shard's nodes. The layout table then tells you what each mode can achieve before you change any configuration. For a typical three-zone ElastiCache deployment that points to AZ_AFFINITY_SLAVES_AND_MASTER with a faster replica failure detector, and leaves one question: whether your primaries can absorb one zone's reads.
If you are still deciding on the engine, Valkey vs Redis compares the two, and migrating from Redis to Valkey in Java covers the move itself. The AZ read modes are part of open-source Redisson. Compare editions in the feature comparison, or get started with Redisson.