Troubleshooting

This guide covers the most common cache-kit issues, how to diagnose them, and how to fix them.


Diagnostic Workflow

Issue Observed
    ↓
Identify Category (Connection? Performance? Data?)
    ↓
Gather Logs & Metrics
    ↓
Check Backend Health
    ↓
Apply Fix
    ↓
Verify Resolution

Tools You'll Need

# Redis diagnosis
redis-cli
redis-cli --latency
redis-cli --stat

# Memcached diagnosis
echo "stats" | nc localhost 11211
memcached-tool localhost:11211

# Application logs
grep "cache" app.log | grep "error"

# System metrics
top
vmstat
netstat

Common Issues & Solutions

Issue 1: Low Cache Hit Rate (< 30%)

Symptoms:

  • Hit rate stuck at 10-20%
  • Cache size not growing
  • High database load despite caching

Possible Causes:

  1. Keys are not being reused (different key each time)
  2. TTL is too short (entries expire quickly)
  3. Cache keys are non-deterministic
  4. Cache is being cleared unexpectedly
  5. New users/data not being cached

Diagnosis Steps

1. Check hit rate and TTL

# Check hit rate from metrics
# Check TTL: redis-cli TTL "user:123"
# -1 = no expiration (problem!), -2 = key doesn't exist

2. Verify keys are deterministic

// ✅ Good: Deterministic key
fn cache_key(&self) -> String { self.id.clone() }

// ❌ Bad: Non-deterministic (creates new key each time)
fn cache_key(&self) -> String {
    format!("{}:{}", self.id, SystemTime::now().timestamp())
}

3. Verify using Refresh strategy (not Fresh)

// ✅ Correct: Cache with DB fallback
expander.with(&mut feeder, &repo, CacheStrategy::Refresh)?;

Solutions

1. Set appropriate TTL

let expander = CacheExpander::builder()
    .with_backend(backend)
    .with_ttl(Duration::from_secs(3600))  // 1 hour
    .build();

2. Ensure keys are deterministic - Use entity ID, not timestamps or random values

3. Use Refresh strategy - CacheStrategy::Refresh (not Fresh) to allow DB fallback

Issue 2: Backend Connection Timeouts

Symptoms:

  • Request timeouts after N milliseconds
  • "Connection refused" errors
  • Pool exhaustion errors
  • p99 latency spikes

Possible Causes:

  1. Backend (Redis/Memcached) is down
  2. Network connectivity issue
  3. Connection pool size is too small
  4. Timeout is set too aggressively

Diagnosis Steps

1. Verify backend is running

redis-cli ping  # Should return PONG
# Or: echo "stats" | nc localhost 11211  # For Memcached

2. Check network and latency

nc -zv localhost 6379  # Test connectivity
redis-cli --latency     # Should be < 1ms (good), > 10ms (slow)

3. Check pool size and timeout config - Look for "pool exhausted" errors in logs

Solutions

1. Restart backend - docker restart redis_container or redis-cli shutdown && redis-server

2. Increase pool size - Use formula: (CPU_cores × 2) + 1

let config = RedisConfig {
    pool_size: (num_cpus::get() * 2 + 1) as u32,
    ..Default::default()
};

3. Increase timeout - Set connection_timeout: Duration::from_secs(10) (not 1-2 seconds)


Issue 3: High Memory Usage

Symptoms:

  • Cache backend consuming GB of RAM
  • OOM killer triggering
  • Eviction errors from backend
  • Request latency increasing

Possible Causes:

  1. Entries are too large (whole objects)
  2. TTL not set (entries never expire)
  3. Too many unique keys (unbounded growth)
  4. No eviction policy configured
  5. Memory leak in application

Diagnosis Steps

1. Check memory usage and key count

redis-cli INFO memory    # Check used_memory_human vs maxmemory
redis-cli DBSIZE         # Total key count
redis-cli --bigkeys      # Find large entries (>512 bytes = problem)

2. Check eviction policy

redis-cli CONFIG GET maxmemory-policy
# Should be "allkeys-lru" (good), not "no-eviction" (bad)

Solutions

1. Set TTL - Ensure entries expire: .with_ttl(Duration::from_secs(3600))

2. Reduce entry size - Cache only needed fields, exclude large blobs (images, passwords)

3. Configure eviction policy

redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru

Issue 4: Serialization Errors

Symptoms:

  • "Serialization failed" errors
  • "Version mismatch" errors
  • "Invalid magic header" errors
  • Some requests fail, others work

Possible Causes:

  1. Entity type changed (schema mismatch)
  2. Corrupted cache entry
  3. Type contains unsupported fields (e.g., Decimal)
  4. Different serialization formats

Diagnosis Steps

1. Check error logs

grep -i "serialization" app.log | head -20

2. Check for unsupported types or schema changes

// ❌ Problem: Decimal not supported
struct User { balance: rust_decimal::Decimal }

// ❌ Problem: Schema changed (added/removed fields)
// Old: struct User { id, name }
// New: struct User { id, name, email }  // Added field!

Solutions

1. Clear affected entries

redis-cli DEL "user:123"  # Or: redis-cli KEYS "user:*" | xargs redis-cli DEL

2. Replace unsupported types - Use i64 for Decimal (store as cents), or use cache-specific DTOs


Logging Setup

Enable debug logging for troubleshooting:

RUST_LOG=cache_kit=debug cargo run

Use structured logging with context:

info!(user_id = %user_id, cache_hit = hit, "Cache operation completed");

See Monitoring Guide for detailed logging setup.


Health Checks

Implement health checks to monitor cache availability:

async fn health_check(cache: &mut CacheExpander<RedisBackend>) -> Result<HealthStatus> {
    let start = Instant::now();
    match cache.health_check().await {
        Ok(true) => {
            let latency = start.elapsed();
            if latency > Duration::from_millis(100) {
                Ok(HealthStatus::Degraded)
            } else {
                Ok(HealthStatus::Healthy)
            }
        }
        _ => Ok(HealthStatus::Unhealthy),
    }
}

See Monitoring Guide for detailed health check implementation.


Production Troubleshooting Checklist

Use this checklist when issues occur:

Cache Issues

  • [ ] Is Redis/Memcached running? (redis-cli ping)
  • [ ] Is network connectivity OK? (nc -zv localhost 6379)
  • [ ] Are connection pool metrics available?
  • [ ] What's the hit rate? (< 20% = investigate TTL/keys)
  • [ ] Are there serialization errors? (check entity types)
  • [ ] Is memory usage growing unbounded? (check TTL)
  • [ ] Are cache keys deterministic? (check key generation)

Network Issues

  • [ ] Is backend reachable? (netstat or ss)
  • [ ] What's the latency? (redis-cli --latency)
  • [ ] Are there packet drops? (netstat -s)
  • [ ] Is there network congestion?
  • [ ] Did firewall rules change?

Application Issues

  • [ ] Are error logs being generated? (grep error app.log)
  • [ ] Is the cache fallback code working?
  • [ ] Are metrics being exported?
  • [ ] Is the database under load?
  • [ ] Did schema change recently?

System Issues

  • [ ] CPU usage normal? (top)
  • [ ] Memory usage normal? (free -h)
  • [ ] Disk space available? (df -h)
  • [ ] System load average? (uptime)
  • [ ] Are there OOM killings? (dmesg | tail)

Error Handling Best Practices

Always provide fallbacks - Cache failures shouldn't break your application:

match cache.with(&mut feeder, &repo, CacheStrategy::Refresh) {
    Ok(_) => feeder.user,
    Err(Error::BackendError(_)) => repo.fetch_by_id(&user_id).ok().flatten(),  // DB fallback
    Err(Error::SerializationError(_)) => {
        cache.with(&mut feeder, &repo, CacheStrategy::Invalidate).ok();
        repo.fetch_by_id(&user_id).ok().flatten()  // Invalidate and refetch
    }
    Err(e) => {
        error!("Cache error: {}", e);
        repo.fetch_by_id(&user_id).ok().flatten()
    }
}

Never panic on cache errors - Use .map_err() or match, never .expect()

Log with context, not sensitive data - Log IDs only: error!("Failed to cache user {}: {}", user.id, e)


Next Steps