Use this guide to troubleshoot HybridCache and Redis caching issues.
Quick Path (80% of issues)
// turbo
- •
Check Redis is running:
bashdocker ps | grep redis && redis-cli ping
Should see:
PONG - •
Check cache tags match between
GetOrCreateAsyncandRemoveByTagAsync - •
Check cache key includes all params (page, culture, userId)
- •
Check
RemoveByTagAsyncis called after mutations
If still broken, continue to full debugging below.
Symptoms
- •✗ Data not being cached (always fetching from DB)
- •✗ Stale data after updates (cache not invalidating)
- •✗ Cache misses when should be hits
- •✗ Redis connection errors
Related Skills
Prerequisites:
- •
/start-solution- Solution must be running to debug cache
First Steps:
- •
/verify-feature- Run basic checks before caching-specific debugging
Related Debugging:
- •
/debug-sse- If issue seems SSE-related instead of cache - •
/doctor- Verify Docker and Redis are installed
After Fixing:
- •
/verify-feature- Confirm caching works correctly - •
/scaffold-test- Add tests for cache scenarios
Debugging Steps
1. Verify Cache Configuration
Check appsettings.json for cache settings:
{
"ConnectionStrings": {
"cache": "localhost:6379" // Redis connection string
}
}
Test Redis connection:
# Check if Redis is running docker ps | grep redis # Test connection redis-cli ping # Expected: PONG
If Redis isn't running:
- •Start via Aspire:
aspire run - •Or manually:
docker run -p 6379:6379 redis:latest
2. Verify HybridCache Setup
Check Program.cs in ApiService:
// ✅ Correct - HybridCache configured
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(1)
};
});
// Add Redis (if using distributed cache)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("cache");
});
// ✗ Wrong - missing configuration
// (no AddHybridCache call)
3. Verify Cache Usage in Code
Check endpoint is using cache:
// ✅ Correct - using cache with tags
public static async Task<IResult> GetBooks(
IDocumentStore store,
HybridCache cache, // ✅ Injected
CancellationToken cancellationToken = default)
{
var books = await cache.GetOrCreateAsync(
"books:list", // Cache key
async entry =>
{
entry.SetOptions(new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5)
});
await using var session = store.QuerySession();
return await session.Query<BookProjection>().ToListAsync();
},
tags: [CacheTags.BookList], // ✅ Tags for invalidation
cancellationToken: cancellationToken
);
return Results.Ok(books);
}
// ✗ Wrong - no caching
public static async Task<IResult> GetBooks(
IDocumentStore store,
CancellationToken cancellationToken = default)
{
await using var session = store.QuerySession();
var books = await session.Query<BookProjection>().ToListAsync();
return Results.Ok(books);
}
4. Verify Cache Invalidation
Check cache is invalidated after mutations:
// In Handler or Endpoint
public static async Task<IResult> UpdateBook(
UpdateBookCommand cmd,
IDocumentSession session,
HybridCache cache,
CancellationToken cancellationToken)
{
// ... mutation logic ...
// ✅ Correct - invalidate by tag
await cache.RemoveByTagAsync(CacheTags.BookList, cancellationToken);
return Results.Ok();
}
// ✗ Wrong - cache never invalidated
// (no RemoveByTagAsync call)
5. Check Cache Tags Definition
Verify cache tags are defined in constants:
// src/ApiService/BookStore.ApiService/Infrastructure/CacheTags.cs
public static class CacheTags
{
public const string BookList = "book-list";
public const string BookDetails = "book-details";
public const string AuthorList = "author-list";
// ❌ Missing: Your resource tags
}
If tags are missing:
- •Add const string for your resource
- •Use consistent naming pattern
- •Use tags in both
GetOrCreateAsyncandRemoveByTagAsync
6. Verify Cache Keys Include All Parameters
Cache keys must include all query parameters:
// ✅ Correct - includes page, pageSize, culture
var cacheKey = $"books:page:{page}:size:{pageSize}:culture:{culture}";
var books = await cache.GetOrCreateAsync(
cacheKey,
async entry => { /* fetch data */ },
tags: [CacheTags.BookList],
cancellationToken: cancellationToken
);
// ✗ Wrong - missing parameters (same key for different pages)
var cacheKey = "books:list"; // Doesn't include page/size!
7. Test Cache Behavior
Test cache hit:
# Request 1 - cache miss curl http://localhost:5000/api/books # Request 2 - should be cache hit (faster) curl http://localhost:5000/api/books
Measure response times:
- •First request: ~100-500ms (DB query)
- •Second request: ~1-10ms (cache hit)
If both requests are slow:
- •Cache isn't working
- •Check HybridCache configuration
- •Verify cache key is consistent
8. Check Redis with redis-cli
Connect to Redis and inspect:
# Connect to Redis redis-cli # List all keys KEYS * # Get cache entry (if using Redis serialization) GET "books:list" # Check TTL (time to live) TTL "books:list" # Flush all cache (for testing) FLUSHALL
9. Monitor Cache Metrics
Check Aspire Dashboard:
- •Go to Traces
- •Look for cache operations
- •Check hit/miss rates
- •Monitor cache latency
Add logging to debug:
public static async Task<IResult> GetBooks(
IDocumentStore store,
HybridCache cache,
ILogger<Program> logger, // Add logger
CancellationToken cancellationToken = default)
{
logger.LogInformation("Fetching books from cache");
var books = await cache.GetOrCreateAsync(
"books:list",
async entry =>
{
logger.LogInformation("Cache MISS - fetching from DB");
await using var session = store.QuerySession();
return await session.Query<BookProjection>().ToListAsync();
},
tags: [CacheTags.BookList],
cancellationToken: cancellationToken
);
logger.LogInformation("Returning {Count} books", books.Count);
return Results.Ok(books);
}
Common Issues & Fixes
Issue: Cache Always Misses
Symptom: Every request fetches from DB
Diagnosis:
- •Add logging to cache callback (see above)
- •Check if callback always executes
Fixes:
- •Verify Redis is running and connected
- •Check cache key is consistent across requests
- •Ensure HybridCache is registered in DI
- •Verify no exceptions in cache retrieval
Issue: Stale Data After Updates
Symptom: Updates don't reflect until cache expires
Diagnosis:
- •Check if
RemoveByTagAsyncis called - •Verify tags match between get and invalidate
Fixes:
// ✅ Correct - matching tags
await cache.GetOrCreateAsync("key", ..., tags: [CacheTags.BookList]);
await cache.RemoveByTagAsync(CacheTags.BookList);
// ✗ Wrong - mismatched tags
await cache.GetOrCreateAsync("key", ..., tags: [CacheTags.BookList]);
await cache.RemoveByTagAsync(CacheTags.BookDetails); // Wrong tag!
Issue: Different Users See Same Data (Cache Poisoning)
Symptom: User A sees User B's data
Diagnosis:
- •Cache key doesn't include user ID or culture
Fix:
// ✅ Correct - user-specific cache key
var userId = httpContext.User.FindFirst("sub")?.Value;
var cacheKey = $"cart:{userId}";
// ✗ Wrong - shared cache key
var cacheKey = "cart"; // Same for all users!
Issue: Redis Connection Errors
Symptom: StackExchange.Redis.RedisConnectionException
Diagnosis:
- •Check Redis container status:
docker ps | grep redis - •Verify connection string in
appsettings.json - •Check network between app and Redis
Fixes:
- •Restart Redis container
- •Check firewall rules
- •Verify Aspire resource configuration in AppHost
Issue: Localized Data Not Cached Correctly
Symptom: Wrong language data returned
Diagnosis:
- •Cache key doesn't include culture
Fix:
// ✅ Correct - culture in cache key
var culture = CultureInfo.CurrentUICulture.Name;
var cacheKey = $"books:list:culture:{culture}";
// ✗ Wrong - missing culture
var cacheKey = "books:list";
Issue: Memory Leak
Symptom: Memory grows over time
Diagnosis:
- •Cache expiration too long
- •Too many unique cache keys
Fixes:
// ✅ Correct - reasonable expiration
entry.SetOptions(new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5), // Distributed cache
LocalCacheExpiration = TimeSpan.FromMinutes(1) // In-memory cache
});
// ✗ Wrong - cache never expires
entry.SetOptions(new HybridCacheEntryOptions
{
Expiration = TimeSpan.MaxValue // Never expires!
});
Verification Checklist
- • Redis container is running (
docker ps) - • Connection string configured in
appsettings.json - •
AddHybridCache()called inProgram.cs - • Cache injected in endpoint (
HybridCache cache) - • Cache key includes all query parameters
- • Cache tags defined and used consistently
- •
RemoveByTagAsynccalled after mutations - • Cache expiration times are reasonable
- • Logging added to debug cache behavior
- • Metrics monitored in Aspire dashboard
Debugging Tools
Redis CLI:
redis-cli > KEYS * # List all keys > GET "key" # Get value > TTL "key" # Check expiration > FLUSHALL # Clear all cache
Aspire Dashboard:
- •Traces → Look for cache operations
- •Metrics → Monitor hit/miss rates
- •Logs → Search for cache-related errors
Logging:
logger.LogInformation("Cache key: {Key}", cacheKey);
logger.LogInformation("Cache tags: {Tags}", string.Join(", ", tags));
logger.LogInformation("Cache callback executed (MISS)");
Performance Tips
- •✅ Use short local cache TTL (1-2 minutes)
- •✅ Use longer distributed cache TTL (5-15 minutes)
- •✅ Include culture in cache keys for localized data
- •✅ Use tag-based invalidation for complex scenarios
- •✅ Monitor cache hit rates (target: >80%)
- •❌ Don't cache user-specific data without user ID in key
- •❌ Don't use very long expirations (causes stale data)
Related Skills
First Steps:
- •
/verify-feature- Run basic checks before caching-specific debugging
Related Debugging:
- •
/debug-sse- If issue seems SSE-related instead of cache - •
/doctor- Verify Docker and Redis are installed
After Fixing:
- •
/verify-feature- Confirm caching works correctly - •
/scaffold-test- Add tests for cache scenarios
See Also:
- •scaffold-read - Cache implementation patterns
- •scaffold-write - Cache invalidation on mutations
- •caching-guide - HybridCache configuration and patterns
- •ApiService AGENTS.md - HybridCache configuration