Caffeine supports multiple time-based expiration strategies to automatically remove stale entries from the cache.
Fixed expiration
Remove entries after a fixed duration has elapsed.
Expire after write
Remove entries after a fixed time since creation or last update:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterWrite ( Duration . ofMinutes ( 30 ))
. build ();
You can also specify duration with time units:
import java.util.concurrent.TimeUnit;
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterWrite ( 30 , TimeUnit . MINUTES )
. build ();
Reference: Caffeine.java:647, Caffeine.java:672
The expiration timer resets whenever an entry is updated via put(), putAll(), or Cache.asMap().put().
Expire after access
Remove entries after a fixed time since last read or write:
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterAccess ( Duration . ofMinutes ( 30 ))
. build ();
Access time is reset by:
All cache read operations: get(), getIfPresent(), getAll(), getAllPresent()
All cache write operations: put(), putAll()
Map operations: Cache.asMap().get(), Cache.asMap().put()
Operations on the collection views of asMap() (like iterating over keySet()) do NOT reset access time.
Reference: Caffeine.java:709, Caffeine.java:737
Variable expiration
Calculate expiration time individually for each entry using an Expiry policy.
Custom expiry
Implement Expiry for complete control:
import com.github.benmanes.caffeine.cache.Expiry;
Expiry < String , Session > expiry = new Expiry <>() {
@ Override
public long expireAfterCreate ( String key , Session session , long currentTime ) {
// Expire based on session type
return session . isGuest ()
? TimeUnit . MINUTES . toNanos ( 5 )
: TimeUnit . HOURS . toNanos ( 1 );
}
@ Override
public long expireAfterUpdate ( String key , Session session ,
long currentTime , long currentDuration ) {
// Reset expiration on update
return currentTime + TimeUnit . MINUTES . toNanos ( 30 );
}
@ Override
public long expireAfterRead ( String key , Session session ,
long currentTime , long currentDuration ) {
// Keep current expiration, don't reset on read
return currentDuration;
}
};
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfter (expiry)
. build ();
Reference: Expiry.java:58, Expiry.java:76, Expiry.java:94
expireAfter() is mutually exclusive with expireAfterAccess() and expireAfterWrite().
Expire on creation
Set expiration only when entries are created:
import com.github.benmanes.caffeine.cache.Expiry;
Expiry < String , Document > expiry = Expiry . creating ((key, document) ->
Duration . ofHours ( document . getTTL ())
);
Cache < String , Document > cache = Caffeine . newBuilder ()
. expireAfter (expiry)
. build ();
This policy does not reset expiration on update or read.
Reference: Expiry.java:108
Expire on write
Reset expiration when entries are created or updated:
Expiry < String , Document > expiry = Expiry . writing ((key, document) ->
Duration . ofHours ( document . getTTL ())
);
Cache < String , Document > cache = Caffeine . newBuilder ()
. expireAfter (expiry)
. build ();
This policy does not reset expiration on read.
Reference: Expiry.java:124
Expire on access
Reset expiration on any read or write:
Expiry < String , Session > expiry = Expiry . accessing ((key, session) ->
Duration . ofMinutes ( session . getIdleTimeout ())
);
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfter (expiry)
. build ();
Reference: Expiry.java:140
Expiration policy operations
Inspect and modify expiration settings at runtime.
Fixed expiration policy
Access fixed expiration policies:
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterWrite ( Duration . ofMinutes ( 30 ))
. build ();
cache . policy (). expireAfterWrite (). ifPresent (expiration -> {
// Get current expiration duration
Duration duration = expiration . getExpiresAfter ();
// Change expiration at runtime
expiration . setExpiresAfter ( Duration . ofMinutes ( 45 ));
// Get entry age
expiration . ageOf ( "session123" ). ifPresent (age ->
System . out . println ( "Age: " + age)
);
});
Reference: Policy.java:398, Policy.java:421
Similarly for expireAfterAccess():
cache . policy (). expireAfterAccess (). ifPresent (expiration -> {
expiration . setExpiresAfter ( Duration . ofMinutes ( 15 ));
});
Reference: Policy.java:113
Variable expiration policy
Control per-entry expiration:
Cache < String , Document > cache = Caffeine . newBuilder ()
. expireAfter ( Expiry . creating ((k, v) -> Duration . ofHours ( 1 )))
. build ();
cache . policy (). expireVariably (). ifPresent (expiration -> {
// Get expiration time for specific entry
expiration . getExpiresAfter ( "doc123" ). ifPresent (duration ->
System . out . println ( "Expires in: " + duration)
);
// Change expiration for specific entry
expiration . setExpiresAfter ( "doc123" , Duration . ofHours ( 2 ));
// Put with custom expiration
expiration . put ( "doc456" , document, Duration . ofMinutes ( 30 ));
// Put if absent with custom expiration
expiration . putIfAbsent ( "doc789" , document, Duration . ofMinutes ( 30 ));
});
Reference: Policy.java:542, Policy.java:571, Policy.java:637
Expiration order inspection
View entries by expiration order:
import java.util.Map;
cache . policy (). expireAfterWrite (). ifPresent (expiration -> {
// Get oldest entries (most likely to expire soon)
Map < String , Session > oldest = expiration . oldest ( 100 );
// Get youngest entries (least likely to expire soon)
Map < String , Session > youngest = expiration . youngest ( 100 );
});
Reference: Policy.java:450, Policy.java:494
Scheduled expiration
Configure prompt removal of expired entries using a scheduler.
System scheduler
import com.github.benmanes.caffeine.cache.Scheduler;
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterWrite ( Duration . ofMinutes ( 30 ))
. scheduler ( Scheduler . systemScheduler ())
. build ();
The system scheduler uses a shared, system-wide scheduled thread pool to remove expired entries promptly.
Reference: Caffeine.java:390
Custom scheduler
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
ScheduledExecutorService executor =
Executors . newScheduledThreadPool ( 1 );
Scheduler scheduler = (executor, task, delay, unit) -> {
return executor . schedule (task, delay, unit);
};
Cache < String , Session > cache = Caffeine . newBuilder ()
. expireAfterAccess ( Duration . ofMinutes ( 15 ))
. scheduler (scheduler)
. executor (executor)
. build ();
The scheduler provides best-effort removal. The minimum delay between executions is ~1 second (2^30 nanoseconds). Without a scheduler, expired entries are removed during cache maintenance.
Expiration behavior
Understand how expiration interacts with cache operations.
Visibility
Expired entries:
May be counted in estimatedSize() until maintenance runs
Are never visible to read operations
Are never visible to write operations
Are removed during periodic maintenance
// Expired entries return null
Session session = cache . getIfPresent ( "expired-key" ); // null
// Force maintenance to remove expired entries
cache . cleanUp ();
long size = cache . estimatedSize (); // May decrease after cleanUp
Reference: Caffeine.java:104
Ticker
Customize the time source for testing:
import com.github.benmanes.caffeine.cache.Ticker;
class FakeTicker implements Ticker {
private long nanos = 0 ;
public void advance ( Duration duration ) {
nanos += duration . toNanos ();
}
@ Override
public long read () {
return nanos;
}
}
FakeTicker ticker = new FakeTicker ();
Cache < String , String > cache = Caffeine . newBuilder ()
. expireAfterWrite ( Duration . ofMinutes ( 30 ))
. ticker (ticker)
. build ();
cache . put ( "key" , "value" );
ticker . advance ( Duration . ofMinutes ( 31 ));
cache . cleanUp ();
assertNull ( cache . getIfPresent ( "key" ));
Reference: Caffeine.java:888
Best practices
Choose the right expiration policy
Use expire after write for data with a fixed freshness period (e.g., auth tokens)
Use expire after access for data that becomes stale when unused (e.g., sessions)
Use variable expiration when entries have different lifetimes based on their content
Optimize expiration duration
Balance freshness requirements against backend load
Use shorter durations for high-churn data
Consider combining with refresh for better availability
Monitor missCount to detect if expiration is too aggressive
Configure a Scheduler for prompt removal if memory is critical
Call cleanUp() manually if you need immediate cleanup
Remember that expired entries are invisible even before removal
Variable expiration considerations
Return Long.MAX_VALUE to indicate entries should never expire
Return currentDuration in update/read methods to keep existing expiration
Keep expiry calculations fast (they run on every access)
Use helper methods like Expiry.creating() for simpler cases
Refresh - Reload entries before they expire
Removal - Handle expiration notifications
Statistics - Monitor expiration patterns