Why Your Spring Boot Redis Cache Never Expires: Hits, Misses, TTL and Eviction as a State Machine
▶ Watch on YouTube & subscribe to The Stack Underflow
You add one annotation, @Cacheable, and the next call with the same argument skips your method and the database. Behind it, Spring computes a key, asks Redis, stores copies, and has to cope when data changes or Redis goes away. The video follows all of it as one hierarchical state machine: a Spring Boot app on AKS caching in Azure Managed Redis, from startup to one key’s life inside Redis. This page is the written companion.
The one-line version: with Spring Boot’s defaults, a cached entry never expires, and Azure’s default eviction policy never evicts it. Set a TTL.
Last verified against the Spring Framework, Spring Boot and Spring Data Redis docs, redis.io and Microsoft Learn: 1 October 2026. Scope: Spring Boot 4.1.1, Spring Framework 7.0.9, Spring Data Redis 4.1.1 with the Lettuce client, Azure Managed Redis, from AKS. Not covered: reactive caching, Redis Cluster internals, client-side caching.
The pegs
From the restaurant picture (memory aids, not facts): your database is the kitchen, and Redis is the warming shelf by the pass, where ready dishes wait.
| Word | Memory peg | What it actually is |
|---|---|---|
| Redis | The warming shelf by the pass | An in-memory store holding copies, not the truth |
| Hit | The dish is already on the shelf | The key exists; the copy is returned and your method never runs |
| Miss | Cook it, put a copy on the shelf | The method runs and its result is stored |
| TTL | The timer card on each plate | A key’s time to live; when it runs out, the key expires |
| Eviction | Clearing old plates when the shelf is full | Removing keys when memory reaches maxmemory |
@CacheEvict | Throwing a plate away: the recipe changed | Deleting the copy after the data changes |
Which Azure Redis?
Azure Cache for Redis is being retired: the Basic, Standard and Premium tiers on 30 September 2028 (remaining instances are disabled from 1 October 2028), and the Enterprise and Enterprise Flash tiers on 31 March 2027. In Azure’s public cloud, since 1 April 2026, only existing customers can still create Basic, Standard or Premium caches; in July 2026 Microsoft removed the block it had planned for existing customers in October 2026. Microsoft recommends moving to Azure Managed Redis now, and the video uses it.
Chapter 1: startup
- Two starters. In Boot 4,
spring-boot-starter-data-redisbrings Lettuce, andspring-boot-starter-cachebrings the cache auto-configuration. Put@EnableCachingon a configuration class; Spring Boot’s docs say to avoid adding it to the main application class. - The connection. Lettuce connects using the
spring.data.redis.*properties, orlocalhost:6379by default. - Azure Managed Redis. It uses port 10000, with TLS required by default, and caches use Microsoft Entra ID by default. It is clustered by default with the OSS cluster policy, which “requires your client library to support the Redis Cluster API”, so set Spring’s cluster nodes (
spring.data.redis.cluster.nodes), or choose the Enterprise cluster policy, which “makes Azure Managed Redis look nonclustered to users”. - The cache manager. With caching enabled and Redis configured, Boot creates a
RedisCacheManager, unless you defined your ownCacheManager. Your beans get caching proxies. - Startup doesn’t empty Redis. Copies stored by earlier pods can still be on the shelf.
Chapter 2: one request through the proxy
bookService.findById(42) carries @Cacheable("books"), so the call goes through Spring’s caching proxy first. The proxy talks to Redis before your method runs, after it, or instead of it. This is cache-aside, run for you: look on the shelf first, cook only on a miss. In proxy mode, only calls coming in from outside the bean are intercepted; a self-call skips the cache.
Chapter 3: a read with @Cacheable
- The key. With one parameter, Spring’s default key is that parameter:
42. Redis keys are prefixed with the cache name, so the key isbooks::42and two caches never share a key. - GET. Redis replies with the stored bytes, or nothing.
- Hit. Spring turns the bytes back into a
Book, with Java serialization (JdkSerializationRedisSerializer) by default, and returns it. Your method is never called. JSON serializers exist too, and Spring Data Redis warns against Java deserialization in untrusted environments. If the bytes no longer fit your class, say after a new version is deployed, deserialization throws, like any other cache error. - Miss. Your method queries the database and returns a
Book. If it throws, nothing is cached. - Store. By default even a
nullresult is cached (a null marker;cache-null-valuesdefaults to true). Anunlessexpression such as#result == nullvetoes the store after the method runs; a falseconditionskips the cache entirely, even the lookup. Spring stores the copy with a SET, carrying the TTL if you set one. By default, entries never expire.
The stampede
The common informal name for many callers missing the same key at once, each one querying the database. By default Spring’s cache abstraction locks nothing. sync = true is, in Spring’s own words, “effectively a hint”, and Spring Data Redis’s default cache writer is lock-free, so it takes no lock. The opt-in locking writer keeps one lock key per cache in Redis (books~lock), so it works across pods, but locking “applies on the cache level, not per cache entry”: it blocks the whole cache, and by default the lock never expires.
Chapter 4: a write
The copy on the shelf doesn’t change when the kitchen changes the recipe; your code decides what happens to it.
@CacheEvict, the common choice. The method updates the database; after it completes successfully, the entry is deleted. If the method throws, nothing is evicted. The next read misses and loads the new value.- Order matters. Update the database first, then evict. Microsoft’s cache-aside guidance explains why: evict first, and a reader can miss in the gap, load the old value, and put it back.
- A failed delete leaves the old plate until its timer runs out, or forever without one. That’s why a TTL is your safety net.
@CachePutalways runs the method and writes its result into the cache, so readers see the new value at once, but only if it uses the same key (the book’s id, not theBookobject) and returns exactly what reads cache. Spring strongly discourages putting@CachePutand@Cacheableon the same method.
Chapter 5: when Redis fails
Redis is a separate server over the network. By default, a cache error fails your request, even when the database is fine: Spring says any exception in a cache operation “is thrown back at the client”, and the default SimpleCacheErrorHandler rethrows.
- Lettuce reconnects automatically. Meanwhile a command can wait up to its timeout: 60 seconds (Lettuce’s default) unless you set
spring.data.redis.timeout. Then Spring Data Redis throws a connection-failure or query-timeout exception. - With a handler that only logs, such as
LoggingCacheErrorHandler, a failed read counts as a miss and your method goes to the database; a failed store or delete is logged and the request carries on.
spring.cache.redis.time-to-live=10m
spring.data.redis.timeout=2s
Register the logging handler through a CachingConfigurer if a cache outage should degrade to database reads rather than errors. (The values above are examples, not recommendations.)
Chapter 6: inside Redis, one key’s life
The key books::42 can go four ways, and each time the app just sees a miss:
- Delete.
@CacheEvictremoves it at once. A SET replaces the value and clears any old timer. - Expiry. When its TTL runs out, reads no longer see it. Redis removes expired keys passively, when a client touches one, and actively, by periodically testing a few random keys that have an expiration.
- Eviction. When memory reaches
maxmemory, the eviction policy picks keys. Open-source Redis, by default, has no memory limit on 64-bit systems and usesnoeviction. Azure Managed Redis defaults tovolatile-lru: only keys with a TTL are eligible, least recently used first. Redis’s docs: thevolatile-xxxpolicies “behave likenoevictionif no keys have an associated expiration”, meaning writes get an error while reads keep working. - Failover. If the primary fails, a replica is promoted. Replication is asynchronous, so a very recent write can be lost. For a cache that’s just a miss, and it’s why Redis is not your database of record.
The video also covers Spring Session: with the Spring Session Data Redis starter, the HttpSession lives in Redis, so any pod behind your AKS service can load it.
Patterns, and when not to cache
| Pattern | What it is | In Spring |
|---|---|---|
| Cache-aside | Read the cache, load on a miss, invalidate on write | @Cacheable + @CacheEvict |
| Write-through | Update the store and the cache in the same write | @CachePut comes closest |
| Write-behind | The caching system writes changes back to the store | Not provided: your method writes the database |
Don’t cache sensitive or security-related data (especially in a shared cache), data that changes constantly or is rarely read after it’s written, or anything you can’t afford to lose. If reads must see a write at once, cache-aside can briefly serve a stale copy; Microsoft suggests write-through.
Pause & Prove
1. You add @Cacheable to findById with Spring Boot’s defaults, on Azure Managed Redis with its default eviction policy. When does books::42 leave Redis?
On its own, never. Boot sets no TTL, so the key never expires, and volatile-lru only evicts keys that have a TTL. It leaves only when something deletes it, such as @CacheEvict after an update, or, rarely, when a failover loses a very recent write. If memory fills and nothing can be evicted, new writes get an out-of-memory error. Set spring.cache.redis.time-to-live.
2. Azure Managed Redis hits its memory limit and none of your keys has a TTL. What happens to the next write?
- The oldest key is evicted. Tempting, but under the default
volatile-lru, keys without a TTL are never candidates, however old they are. (Underallkeys-lru, the least recently used key would go.) - A random key is evicted. That’s
allkeys-random(orvolatile-randomamong keys with a TTL), not the default. - The write gets an out-of-memory error. ✓ With no TTLs,
volatile-lrubehaves likenoeviction: commands that add data get an error, reads keep working. - Redis restarts. The eviction docs describe an error on writes, not a restart.
3. Peg drill
Redis · hit · miss · TTL · eviction · @CacheEvict. (The warming shelf by the pass · the dish is already on the shelf · cook it, put a copy on the shelf · the timer card on each plate · clearing old plates when the shelf is full · throwing a plate away: the recipe changed.)
Before and after this video
- Before: Base64 Is Not Encryption: secrets and cache words, the primer for every cache word here.
- Same bookstore, its database password: How secrets reach a pod in AKS.
Sources
Re-checked on 1 October 2026:
- Spring Boot 4.1.1: Caching:
@EnableCachingplacement,spring-boot-starter-cache,RedisCacheManager, key prefix,time-to-live - Spring Boot v4.1.1 source,
CacheProperties.java(“Entry expiration. By default the entries never expire.”;cacheNullValues = true) andLettuceConnectionConfiguration.java(command timeout only ifspring.data.redis.timeoutis set) - Spring Data Redis 4.1.1: Redis cache: defaults (no key expiration, cache null, prefix, JDK value serializer, non-locking writer), cache-level locking; v4.1.1
DefaultRedisCacheWriter(~lockkey, persistent lock TTL) - Spring Framework 7.0.9: cache annotations: default key,
sync,unless/condition,@CachePut,@CacheEvicttiming, proxy self-invocation, exceptions thrown to the client; v7.0.9Cacheable.syncjavadoc,LoggingCacheErrorHandler,AbstractCacheInvoker(handled get error = miss) - Lettuce 7.5.2 source (
RedisURI.DEFAULT_TIMEOUT = 60,ClientOptions.DEFAULT_AUTO_RECONNECT = true) - Redis: key eviction:
maxmemorydefault, policies,volatile-xxxbehaves likenoeviction - Redis: EXPIRE: passive and active expiry; SET clears the timeout
- Azure Managed Redis: memory management: default
volatile-lru - Azure Managed Redis: architecture: cluster policies, port 10000, 85xx shard ports
- Azure Managed Redis: TLS and Entra authentication: TLS required by default, port 10000, Entra ID by default
- Azure Cache for Redis retirement FAQ and What’s new: retirement dates, creation blocks, “clustered by default”
- Cache-Aside pattern and Caching guidance: order of update and invalidate, stale window, write-through, when not to cache, asynchronous replication on Azure Managed Redis
- From the video’s source check (29 September 2026): Spring Data Redis serializers, Spring Session with Redis, Spring Data Redis
LettuceExceptionConverter
Change notes
- 1 Oct 2026: first published.
Not affiliated with or endorsed by Redis Ltd., Microsoft, or VMware Broadcom (Spring). Found a mistake? Tell us in the video’s comments and we’ll correct this page.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →Prefer email? Get the free newsletter: one failure, traced step by step, about once a week.