Skip to main content
Caffeine can record detailed statistics about cache operations, helping you understand and optimize cache behavior.

Enabling statistics

Statistics recording is disabled by default for performance reasons.

Basic statistics

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

// Perform operations
cache.put("user1", user1);
User user = cache.getIfPresent("user1");

// Retrieve statistics
CacheStats stats = cache.stats();
System.out.println(stats);
Reference: Caffeine.java:1019, Cache.java:198
Recording statistics has a performance cost. Each cache operation requires bookkeeping. Only enable in production if you need the metrics.

Custom stats counter

Provide a custom StatsCounter implementation:
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import com.github.benmanes.caffeine.cache.stats.ConcurrentStatsCounter;
import java.util.function.Supplier;

Supplier<StatsCounter> statsSupplier = () -> new ConcurrentStatsCounter();

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats(statsSupplier)
    .build();
Reference: Caffeine.java:1036

Available metrics

Caffeine tracks comprehensive cache operation statistics.

Request metrics

hitCount
long
Number of times cache lookups returned a cached value.
missCount
long
Number of times cache lookups returned an uncached value or null. Includes both successful and failed loads.
requestCount
long
Total number of cache lookups: hitCount + missCount.
hitRate
double
Ratio of cache hits to total requests: hitCount / requestCount. Returns 1.0 when requestCount is 0.
missRate
double
Ratio of cache misses to total requests: missCount / requestCount. Returns 0.0 when requestCount is 0.
Reference: CacheStats.java:140, CacheStats.java:165
CacheStats stats = cache.stats();

System.out.printf("Requests: %d%n", stats.requestCount());
System.out.printf("Hits: %d (%.2f%%)%n", 
    stats.hitCount(), stats.hitRate() * 100);
System.out.printf("Misses: %d (%.2f%%)%n", 
    stats.missCount(), stats.missRate() * 100);

Load metrics

loadSuccessCount
long
Number of successful cache loads (includes both initial loads and refreshes).
loadFailureCount
long
Number of failed cache loads due to exceptions or returning null.
loadCount
long
Total number of load attempts: loadSuccessCount + loadFailureCount.
totalLoadTime
long
Total time spent loading entries, in nanoseconds.
averageLoadPenalty
double
Average time spent loading entries: totalLoadTime / loadCount. Returns 0.0 when loadCount is 0.
loadFailureRate
double
Ratio of failed loads: loadFailureCount / loadCount. Returns 0.0 when loadCount is 0.
Reference: CacheStats.java:209, CacheStats.java:222, CacheStats.java:249
CacheStats stats = cache.stats();

System.out.printf("Load success: %d%n", stats.loadSuccessCount());
System.out.printf("Load failures: %d (%.2f%%)%n",
    stats.loadFailureCount(), stats.loadFailureRate() * 100);
System.out.printf("Average load time: %.2f ms%n",
    stats.averageLoadPenalty() / 1_000_000.0);

Eviction metrics

evictionCount
long
Number of entries evicted from the cache (does not include manual invalidations).
evictionWeight
long
Sum of weights of evicted entries (for weighted caches).
Reference: CacheStats.java:274, CacheStats.java:284
CacheStats stats = cache.stats();

System.out.printf("Evictions: %d%n", stats.evictionCount());

if (cache.policy().isRecordingStats()) {
    System.out.printf("Eviction weight: %d%n", stats.evictionWeight());
}

Interpreting statistics

Use statistics to optimize cache configuration.

Hit rate analysis

Cache is working well. Consider:
  • Current configuration is appropriate
  • May be able to reduce cache size slightly
  • Monitor for future changes in access patterns
if (stats.hitRate() > 0.90) {
    logger.info("Cache performing well: {}% hit rate", 
        stats.hitRate() * 100);
}

Load performance

Monitor load operations:
CacheStats stats = cache.stats();

// Check load failure rate
if (stats.loadFailureRate() > 0.01) {
    logger.error("High load failure rate: {}%",
        stats.loadFailureRate() * 100);
}

// Check load time
double avgLoadMs = stats.averageLoadPenalty() / 1_000_000.0;
if (avgLoadMs > 100) {
    logger.warn("Slow load operations: {} ms average", avgLoadMs);
}

// Check if caching is beneficial
double hitRate = stats.hitRate();
if (hitRate > 0 && avgLoadMs > 1) {
    double savedMs = stats.hitCount() * avgLoadMs;
    logger.info("Cache saved approximately {} ms", savedMs);
}

Eviction rate

Monitor how frequently entries are evicted:
CacheStats stats = cache.stats();

// Calculate eviction rate
double evictionRate = (double) stats.evictionCount() / 
    (stats.loadSuccessCount() + stats.hitCount());

if (evictionRate > 0.50) {
    logger.warn("High eviction rate: {}% - consider increasing cache size",
        evictionRate * 100);
}

// For weighted caches
if (cache.policy().eviction().isPresent()) {
    long evictedWeight = stats.evictionWeight();
    long totalWeight = cache.policy().eviction().get()
        .weightedSize().orElse(0L);
    logger.info("Evicted weight: {} of total {}", 
        evictedWeight, totalWeight);
}

Statistics operations

Perform calculations with statistics snapshots.

Snapshot and delta

import com.github.benmanes.caffeine.cache.stats.CacheStats;

// Take initial snapshot
CacheStats initial = cache.stats();

// ... perform operations ...

// Take second snapshot
CacheStats current = cache.stats();

// Calculate delta
CacheStats delta = current.minus(initial);

System.out.printf("Requests in period: %d%n", delta.requestCount());
System.out.printf("Hit rate in period: %.2f%%%n", 
    delta.hitRate() * 100);
Reference: CacheStats.java:296

Aggregating statistics

Combine statistics from multiple caches:
CacheStats cache1Stats = cache1.stats();
CacheStats cache2Stats = cache2.stats();

// Aggregate
CacheStats combined = cache1Stats.plus(cache2Stats);

System.out.printf("Total requests: %d%n", combined.requestCount());
System.out.printf("Combined hit rate: %.2f%%%n", 
    combined.hitRate() * 100);
Reference: CacheStats.java:318

Integration with monitoring

Export statistics to monitoring systems.

Prometheus metrics

import io.prometheus.client.Gauge;

// Define metrics
Gauge cacheHitRate = Gauge.build()
    .name("cache_hit_rate")
    .help("Cache hit rate")
    .register();

Gauge cacheSize = Gauge.build()
    .name("cache_size")
    .help("Number of entries in cache")
    .register();

// Update periodically
ScheduledExecutorService executor = 
    Executors.newScheduledThreadPool(1);

executor.scheduleAtFixedRate(() -> {
    CacheStats stats = cache.stats();
    cacheHitRate.set(stats.hitRate());
    cacheSize.set(cache.estimatedSize());
}, 0, 10, TimeUnit.SECONDS);

Custom metrics reporter

import com.github.benmanes.caffeine.cache.stats.StatsCounter;

class MonitoringStatsCounter implements StatsCounter {
    private final MetricsRegistry metrics;
    private final ConcurrentStatsCounter delegate;
    
    MonitoringStatsCounter(MetricsRegistry metrics) {
        this.metrics = metrics;
        this.delegate = new ConcurrentStatsCounter();
    }
    
    @Override
    public void recordHits(int count) {
        delegate.recordHits(count);
        metrics.incrementCounter("cache.hits", count);
    }
    
    @Override
    public void recordMisses(int count) {
        delegate.recordMisses(count);
        metrics.incrementCounter("cache.misses", count);
    }
    
    @Override
    public void recordLoadSuccess(long loadTime) {
        delegate.recordLoadSuccess(loadTime);
        metrics.recordTimer("cache.load.success", loadTime);
    }
    
    @Override
    public void recordLoadFailure(long loadTime) {
        delegate.recordLoadFailure(loadTime);
        metrics.recordTimer("cache.load.failure", loadTime);
    }
    
    @Override
    public void recordEviction(int weight, RemovalCause cause) {
        delegate.recordEviction(weight, cause);
        metrics.incrementCounter("cache.evictions", 1);
        metrics.incrementCounter("cache.evictions." + cause.name(), 1);
    }
    
    @Override
    public CacheStats snapshot() {
        return delegate.snapshot();
    }
}

// Use custom counter
Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats(() -> new MonitoringStatsCounter(metricsRegistry))
    .build();

Best practices

  • Enable in development/staging environments
  • Enable selectively in production for critical caches
  • Disable in high-throughput scenarios if performance is critical
  • Use sampling if overhead is a concern
  • Track hit rate as primary health metric
  • Monitor load times to ensure caching is beneficial
  • Watch eviction count to optimize cache size
  • Alert on sudden changes in hit rate
  • Compare before/after stats when tuning
  • Target > 80% hit rate for most applications
  • Keep average load penalty well above cache overhead
  • Adjust cache size if eviction rate is high
  • Consider if low hit rate indicates wrong access pattern
  • Always enable stats in tests
  • Assert on expected hit/miss patterns
  • Use stats to validate cache behavior
  • Test with realistic access patterns

Common patterns

ScheduledExecutorService scheduler = 
    Executors.newScheduledThreadPool(1);

CacheStats previous = CacheStats.empty();

scheduler.scheduleAtFixedRate(() -> {
    CacheStats current = cache.stats();
    CacheStats delta = current.minus(previous);
    
    logger.info("Cache stats (last minute): " +
        "requests={}, hits={}, hit_rate={:.2%}, " +
        "evictions={}",
        delta.requestCount(),
        delta.hitCount(),
        delta.hitRate(),
        delta.evictionCount());
    
    previous = current;
}, 1, 1, TimeUnit.MINUTES);

Build docs developers (and LLMs) love