Monitoring
Track cache performance and debug issues.
Cache Statistics
The getStats() method returns cumulative cache metrics since application startup:
const stats = await cache.getStats();
/*
{
l1: {
hits: 15234, // L1 cache hits (cumulative)
misses: 1876, // L1 cache misses (cumulative)
size: 523, // Current entries in L1
},
l2: {
hits: 45123, // L2 cache hits (cumulative)
misses: 2341, // L2 cache misses (cumulative)
},
stampedePrevented: 142, // Total stampede events prevented (cumulative)
}
*/Counters are cumulative
hits, misses, and stampedePrevented are cumulative counters — they grow from zero at application startup and never reset. To calculate rates, take the delta between two snapshots over time.
Available Metrics
| Metric | Type | Source | Description |
|---|---|---|---|
l1.hits | Counter | L1 adapter get() | Entry found and not expired |
l1.misses | Counter | L1 adapter get() | Entry not found, or expired on access |
l1.size | Gauge | L1 adapter | Current number of entries in memory |
l2.hits | Counter | L2 adapter get(), getMany(), getSwr() | Entry found in Redis |
l2.misses | Counter | L2 adapter get(), getMany(), getSwr() | Entry not found, or Redis error (fail-open) |
stampedePrevented | Counter | Stampede service protect() | Concurrent request waited instead of loading |
What's NOT in getStats()
The public getStats() returns CacheStats which is a subset of all available stats:
| Stats | Available in getStats()? | How to access |
|---|---|---|
SWR activeRevalidations, enabled, staleTtl | No | @Inject(SWR_MANAGER) → .getStats() |
Stampede activeFlights, totalWaiters, oldestFlight | No (only prevented) | @Inject(STAMPEDE_PROTECTION) → .getStats() |
import { Inject, Injectable } from '@nestjs/common';
import {
STAMPEDE_PROTECTION,
SWR_MANAGER,
type IStampedeProtection,
type ISwrManager,
} from '@nestjs-redisx/cache';
@Injectable()
export class DetailedStatsService {
constructor(
@Inject(STAMPEDE_PROTECTION) private readonly stampede: IStampedeProtection,
@Inject(SWR_MANAGER) private readonly swr: ISwrManager,
) {}
getDetailedStats() {
const stampedeStats = this.stampede.getStats();
// { activeFlights, totalWaiters, oldestFlight, prevented }
const swrStats = this.swr.getStats();
// { activeRevalidations, enabled, staleTtl }
return { stampede: stampedeStats, swr: swrStats };
}
}Calculate Hit Rates
import { Injectable } from '@nestjs/common';
import { CacheService } from '@nestjs-redisx/cache';
@Injectable()
export class MonitoringService {
constructor(private readonly cache: CacheService) {}
async getCacheMetrics() {
const stats = await this.cache.getStats();
const l1Total = stats.l1.hits + stats.l1.misses;
const l2Total = stats.l2.hits + stats.l2.misses;
return {
l1: {
size: stats.l1.size,
hitRate: l1Total > 0 ? (stats.l1.hits / l1Total * 100).toFixed(2) + '%' : 'N/A',
hits: stats.l1.hits,
misses: stats.l1.misses,
},
l2: {
hitRate: l2Total > 0 ? (stats.l2.hits / l2Total * 100).toFixed(2) + '%' : 'N/A',
hits: stats.l2.hits,
misses: stats.l2.misses,
},
stampedePrevented: stats.stampedePrevented,
};
}
}Built-in Metrics & Tracing Integration
The internal cache service has optional integration with MetricsPlugin and TracingPlugin. When these plugins are registered, metrics and traces are collected automatically — no custom code needed.
MetricsPlugin (automatic)
If MetricsPlugin is registered, the cache service automatically increments these Prometheus counters:
| Metric name | Labels | When |
|---|---|---|
redisx_cache_hits_total | layer: 'l1' | L1 cache hit |
redisx_cache_misses_total | layer: 'l1' | L1 cache miss |
redisx_cache_hits_total | layer: 'l2' | L2 cache hit |
redisx_cache_misses_total | layer: 'l2' | L2 cache miss |
redisx_cache_stampede_prevented_total | — | Stampede prevention event |
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { CachePlugin } from '@nestjs-redisx/cache';
import { MetricsPlugin } from '@nestjs-redisx/metrics';
@Module({
imports: [
RedisModule.forRoot({
clients: { host: 'localhost', port: 6379 },
plugins: [
new CachePlugin({ l1: { maxSize: 1000 } }),
new MetricsPlugin(), // Enables automatic metric collection
],
}),
],
})
export class AppModule {}TracingPlugin (automatic)
If TracingPlugin is registered, the cache service automatically creates OpenTelemetry spans:
| Span name | Attributes | When |
|---|---|---|
cache.get | cache.key | Every get() call |
cache.set | cache.key, cache.ttl | Every set() call |
plugins: [
new CachePlugin({ l1: { maxSize: 1000 } }),
new TracingPlugin(), // Enables automatic span creation
],Optional injection
Both integrations use @Optional() @Inject() — if the plugin is not registered, the cache works normally without metrics or traces.
Custom Prometheus (without MetricsPlugin)
If you prefer manual Prometheus integration with prom-client instead of MetricsPlugin:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Gauge, register } from 'prom-client';
import { CacheService } from '@nestjs-redisx/cache';
@Injectable()
export class CacheMetricsService implements OnModuleInit {
private readonly l1HitRate = new Gauge({
name: 'cache_l1_hit_rate',
help: 'L1 cache hit rate (0-1)',
});
private readonly l1Size = new Gauge({
name: 'cache_l1_size',
help: 'Current L1 cache size',
});
private readonly l2HitRate = new Gauge({
name: 'cache_l2_hit_rate',
help: 'L2 cache hit rate (0-1)',
});
private interval: NodeJS.Timeout;
constructor(private readonly cache: CacheService) {}
onModuleInit() {
this.interval = setInterval(() => this.collect(), 10_000);
}
private async collect() {
const stats = await this.cache.getStats();
const l1Total = stats.l1.hits + stats.l1.misses;
const l2Total = stats.l2.hits + stats.l2.misses;
this.l1HitRate.set(l1Total > 0 ? stats.l1.hits / l1Total : 0);
this.l1Size.set(stats.l1.size);
this.l2HitRate.set(l2Total > 0 ? stats.l2.hits / l2Total : 0);
}
getMetrics() {
return register.metrics();
}
}Gauges, not Counters
Use Gauge for hit rates and sizes derived from cumulative stats. If you need Counter for hits/misses, calculate the delta between snapshots — don't pass cumulative values to Counter.inc().
Grafana Queries
Example PromQL queries (when using MetricsPlugin):
# L1 hit rate (over 5 minutes)
rate(redisx_cache_hits_total{layer="l1"}[5m])
/ (rate(redisx_cache_hits_total{layer="l1"}[5m]) + rate(redisx_cache_misses_total{layer="l1"}[5m]))
# L2 hit rate (over 5 minutes)
rate(redisx_cache_hits_total{layer="l2"}[5m])
/ (rate(redisx_cache_hits_total{layer="l2"}[5m]) + rate(redisx_cache_misses_total{layer="l2"}[5m]))
# Stampede prevention rate
rate(redisx_cache_stampede_prevented_total[5m])Logging
The cache service uses NestJS Logger internally. Key log messages:
| Service | Level | Message |
|---|---|---|
WarmupService | LOG | Starting cache warmup for N keys... |
WarmupService | LOG | Cache warmup completed: X succeeded, Y failed (Zms) |
SwrManagerService | DEBUG | Starting revalidation for key: {key} |
SwrManagerService | ERROR | Revalidation failed for key: {key} |
StampedeProtectionService | WARN | Failed to acquire distributed lock: ... |
CacheService | WARN | Invalid cache key "{key}": ... |
To see DEBUG logs, configure NestJS log level:
const app = await NestFactory.create(AppModule, {
logger: ['log', 'error', 'warn', 'debug'],
});Next Steps
- Testing — Test cached services
- Troubleshooting — Debug common issues