Bluetape4k Cache Part 2: Near Cache as a Practical Way to Reduce Redis Round Trips

This is Part 2 of the Bluetape4k Cache series. The full series continues with Part 1: cache module overview, Part 2: Near Cache, Part 3: Near Cache + Exposed strategies, and Part 4: workshop examples.
A remote cache is a good starting point. Multiple JVMs can see the same value. Entries can have TTLs. Mature systems such as Redis come with solid operational tooling.
The catch is in the word remote. Every access carries a network hop, serialization, connection contention, and tail latency. Redis can be fast and still not live under the same physics as an in-process memory read. Physics does not negotiate, even if invited to a meeting.
Near Cache accepts that difference. Frequently read values stay in an in-JVM L1 cache, while shared state and invalidation stay with an L2 cache and its protocol.

Limits of Remote Cache Alone
Section titled “Limits of Remote Cache Alone”Remote cache gives you clear advantages.
- Multiple application instances share the same values.
- Cache state does not disappear just because one JVM restarts.
- TTL, eviction, and memory policies can be managed centrally.
Hot read paths accumulate a different set of costs.
| Cost | Description |
|---|---|
| network RTT | Even localhost Redis is much farther than JVM memory |
| serialization | Encoding and decoding become visible as values grow |
| connection contention | Redis connections and event loops get busy under load |
| tail latency | Average latency may look fine while P99 becomes user-visible |
Near Cache is not “put everything local.” It is “finish hot reads locally, and let the remote cache and its protocol handle shared state and invalidation.”
Redisson Pub/Sub
Section titled “Redisson Pub/Sub”Redisson’s RLocalCachedMap provides a local cached map. Each JVM keeps a local map, and
changes propagate through Pub/Sub so other JVMs invalidate their local entries.
The benefits are clear.
- Users do not have to design the protocol themselves.
- Local cache and invalidation live inside the Redisson API.
- It is more useful for read-heavy services than write-heavy ones.
Related source: RedissonNearCache.kt, LocalCachedMapOptionsExtensions.kt
There is a tradeoff. Pub/Sub is topic-based event propagation. Redis does not automatically know which client read which key. The client and library maintain the invalidation system together. So the simple-looking setup still means “receive the change event and remove the right local entry.”
Lettuce RESP3 CLIENT TRACKING
Section titled “Lettuce RESP3 CLIENT TRACKING”Lettuce NearCache uses Redis RESP3 CLIENT TRACKING. Redis tracks which keys a client has read
and pushes invalidation messages when those keys change.
val cache = LettuceCaches.nearCache<String>(redisClient) { cacheName = "catalog" maxLocalSize = 50_000 frontExpireAfterWrite = Duration.ofMinutes(10) redisTtl = Duration.ofHours(1) useRespProtocol3 = true}Related source: LettuceNearCache.kt, LettuceNearCacheConfig.kt, LettuceNearCacheTrackingTest.kt
The appeal of RESP3 tracking is that the server participates in key tracking. The application uses a fast L1 cache, and Redis pushes invalidation for changed keys. Instead of the client keeping a private diary of what it read, Redis holds the attendance sheet. The attendance sheet still needs care, but it is a better starting point.
Benchmark Results
Section titled “Benchmark Results”bluetape4k-cache-lettuce includes a Near Cache benchmark.
./gradlew :bluetape4k-cache-lettuce:benchmarkRelated source: NearCacheBenchmark.kt, Benchmark.ko.md

Throughput is measured in ops/ms; higher is better. Looking at the source report with its
error ranges, L1 is dramatically faster, while Redis-backed operations cluster around Redis RTT.
| Benchmark | 512B | 4096B | 16384B |
|---|---|---|---|
l1Hit | 65,560 ± 10,861 | 63,458 ± 23,120 | 64,580 ± 9,507 |
l2Hit | 4.067 ± 0.532 | 4.130 ± 0.452 | 3.930 ± 1.370 |
l2Miss | 3.961 ± 1.394 | 3.917 ± 0.784 | 4.208 ± 0.408 |
putSingle | 2.119 ± 0.100 | 2.077 ± 0.276 | 2.014 ± 0.152 |
putAll | 1.038 ± 0.247 | 0.930 ± 0.118 | 0.407 ± 0.281 |
removeSingle | 4.213 ± 0.177 | 4.243 ± 0.352 | 4.164 ± 0.271 |
For the read path, compare l1Hit and l2Hit. L1 hits are around 64K ops/ms. L2 hits and
misses are around 4 ops/ms. If those numbers still make you want to send every hot read to
Redis, the first cache to clear might be the one in your head.
The environment matters: Apple M4 Pro, GraalVM 21, Redis 7 Testcontainers, JMH 1.37, single thread. Absolute values will move on another machine. The conclusion that JVM L1 memory and a Redis-backed path are different classes of work is much harder to move.
Interpretation
Section titled “Interpretation”The benchmark does not say “Redis is slow.” Redis is fast. It says an in-process memory read and a remote operation are not the same race.
Practical interpretation:
- Read-heavy hot keys are good NearCache candidates.
- Write-heavy data needs invalidation cost and staleness tolerance reviewed first.
- Large payloads expose network bandwidth and serialization cost, especially in
putAll. - If the L1 hit ratio is low, NearCache becomes decoration. Decoration does not reduce CPU.
- Without a failure policy, cache becomes a failure propagator.
Selection Guide
Section titled “Selection Guide”| Situation | Recommendation |
|---|---|
| Heavy existing Redisson usage | Redisson local cached map |
| Lettuce-centered stack with Redis RESP3 available | Lettuce NearCache |
| JCache standard API matters | NearJCache / SuspendNearJCache |
| Coroutine service | SuspendNearCacheOperations |
| Redis outage needs graceful degradation | Resilient decorator |
Next, we will connect NearCache to an Exposed repository. Once reads, writes, and write-behind queues enter the picture, cache stops being only a performance feature and becomes a data consistency design. That is the moment cache suddenly gets serious.
Comments
Leave a note or reaction with your GitHub account.