Caching

Ferro provides a unified caching API with support for multiple backends, cache tags for bulk invalidation, and the convenient "remember" pattern for lazy caching.

Configuration

Environment Variables

Configure caching in your .env file:

# Cache driver (memory or redis)
CACHE_DRIVER=memory

# Key prefix for all cache entries
CACHE_PREFIX=myapp

# Default TTL in seconds
CACHE_TTL=3600

# Memory store capacity (max entries)
CACHE_MEMORY_CAPACITY=10000

# Redis URL (required if CACHE_DRIVER=redis)
REDIS_URL=redis://127.0.0.1:6379

Bootstrap Setup

In src/bootstrap.rs, configure caching:

#![allow(unused)]
fn main() {
use ferro::{App, Cache};
use std::sync::Arc;

pub async fn register() {
    // ... other setup ...

    // Create cache from environment variables
    let cache = Arc::new(Cache::from_env().await?);

    // Store in app state for handlers to access
    App::set_cache(cache);
}
}

Manual Configuration

#![allow(unused)]
fn main() {
use ferro::{Cache, CacheConfig};
use std::time::Duration;

// In-memory cache with custom config
let config = CacheConfig::new()
    .with_ttl(Duration::from_secs(1800))
    .with_prefix("myapp");

let cache = Cache::memory().with_config(config);

// Redis cache
let cache = Cache::redis("redis://127.0.0.1:6379").await?;
}

Basic Usage

Storing Values

#![allow(unused)]
fn main() {
use std::time::Duration;

// Store a value with specific TTL
cache.put("user:1", &user, Duration::from_secs(3600)).await?;

// Store with default TTL
cache.put_default("user:1", &user).await?;

// Store forever (10 years TTL)
cache.forever("config:settings", &settings).await?;
}

Retrieving Values

#![allow(unused)]
fn main() {
// Get a value
let user: Option<User> = cache.get("user:1").await?;

if let Some(user) = user {
    println!("Found user: {}", user.name);
}

// Check if key exists
if cache.has("user:1").await? {
    println!("User is cached");
}
}

Removing Values

#![allow(unused)]
fn main() {
// Remove a single key
cache.forget("user:1").await?;

// Remove all cached values
cache.flush().await?;
}

Pull (Get and Remove)

#![allow(unused)]
fn main() {
// Get value and remove it from cache
let session: Option<Session> = cache.pull("session:abc123").await?;
}

Remember Pattern

The remember pattern retrieves a cached value or computes and caches it if missing:

#![allow(unused)]
fn main() {
use std::time::Duration;

// Get from cache or compute if missing
let users = cache.remember("users:active", Duration::from_secs(3600), || async {
    // This only runs if "users:active" is not in cache
    User::where_active().all().await
}).await?;

// Remember forever
let config = cache.remember_forever("app:config", || async {
    load_config_from_database().await
}).await?;
}

This pattern is excellent for:

  • Database query results
  • API responses
  • Expensive computations
  • Configuration that rarely changes

Cache Tags

Tags allow you to group related cache entries for bulk invalidation.

Storing with Tags

#![allow(unused)]
fn main() {
use std::time::Duration;

// Store with a single tag
cache.tags(&["users"])
    .put("user:1", &user, Duration::from_secs(3600))
    .await?;

// Store with multiple tags
cache.tags(&["users", "admins"])
    .put("admin:1", &admin, Duration::from_secs(3600))
    .await?;

// Remember with tags
let user = cache.tags(&["users"])
    .remember("user:1", Duration::from_secs(3600), || async {
        User::find(1).await
    })
    .await?;
}

Flushing Tags

#![allow(unused)]
fn main() {
// Flush all entries tagged with "users"
cache.tags(&["users"]).flush().await?;

// This removes:
// - "user:1" (tagged with ["users"])
// - "admin:1" (tagged with ["users", "admins"])
}

Tag Use Cases

#![allow(unused)]
fn main() {
// Cache user data
cache.tags(&["users", &format!("user:{}", user.id)])
    .put(&format!("user:{}", user.id), &user, ttl)
    .await?;

// Cache user's posts
cache.tags(&["posts", &format!("user:{}:posts", user.id)])
    .put(&format!("user:{}:posts", user.id), &posts, ttl)
    .await?;

// When user is updated, flush their cache
cache.tags(&[&format!("user:{}", user.id)]).flush().await?;

// When any user data changes, flush all user cache
cache.tags(&["users"]).flush().await?;
}

Atomic Operations

Increment and Decrement

#![allow(unused)]
fn main() {
// Increment a counter
let views = cache.increment("page:views", 1).await?;
println!("Page has {} views", views);

// Increment by more than 1
let score = cache.increment("player:score", 100).await?;

// Decrement
let stock = cache.decrement("product:stock", 1).await?;
}

Cache Backends

Memory Store

Fast in-memory caching backed by moka. Best for:

  • Single-server deployments
  • Development/testing
  • Non-critical cache data
#![allow(unused)]
fn main() {
// Default capacity (10,000 entries)
let cache = Cache::memory();

// Custom capacity
let store = MemoryStore::with_capacity(50_000);
let cache = Cache::new(Arc::new(store));
}

The memory store is bounded: when capacity is reached, least-recently-used entries are evicted automatically. Each entry respects its own TTL — expired entries are never returned and are cleaned up proactively by the cache engine. Counters (increment/decrement) share the same capacity bound.

Redis Store

Distributed caching with Redis. Best for:

  • Multi-server deployments
  • Persistent cache (survives restarts)
  • Shared cache across services
#![allow(unused)]
fn main() {
let cache = Cache::redis("redis://127.0.0.1:6379").await?;

// With authentication
let cache = Cache::redis("redis://:password@127.0.0.1:6379").await?;

// With database selection
let cache = Cache::redis("redis://127.0.0.1:6379/2").await?;
}

Enable the Redis backend in Cargo.toml:

[dependencies]
ferro = { version = "0.1", features = ["redis-backend"] }

Example: API Response Caching

#![allow(unused)]
fn main() {
use ferro::{Request, Response, Cache};
use std::sync::Arc;
use std::time::Duration;

async fn get_products(
    request: Request,
    cache: Arc<Cache>,
) -> Response {
    let category = request.param("category")?;

    // Cache key based on category
    let cache_key = format!("products:category:{}", category);

    // Get from cache or fetch from database
    let products = cache.remember(&cache_key, Duration::from_secs(300), || async {
        Product::where_category(&category).all().await
    }).await?;

    Response::json(&products)
}
}

Example: User Session Caching

#![allow(unused)]
fn main() {
use ferro::Cache;
use std::sync::Arc;
use std::time::Duration;

async fn cache_user_session(
    cache: Arc<Cache>,
    user_id: i64,
    session: &UserSession,
) -> Result<(), Error> {
    // Cache with user-specific tag for easy invalidation
    cache.tags(&["sessions", &format!("user:{}", user_id)])
        .put(
            &format!("session:{}", session.id),
            session,
            Duration::from_secs(86400), // 24 hours
        )
        .await
}

async fn invalidate_user_sessions(
    cache: Arc<Cache>,
    user_id: i64,
) -> Result<(), Error> {
    // Flush all sessions for this user
    cache.tags(&[&format!("user:{}", user_id)]).flush().await
}
}

Example: Rate Limiting with Cache

#![allow(unused)]
fn main() {
use ferro::Cache;
use std::sync::Arc;

async fn check_rate_limit(
    cache: Arc<Cache>,
    user_id: i64,
    limit: i64,
) -> Result<bool, Error> {
    let key = format!("rate_limit:user:{}", user_id);

    // Increment the counter
    let count = cache.increment(&key, 1).await?;

    // Set TTL on first request (1 minute window)
    if count == 1 {
        // Note: For production, use Redis SETEX or similar
        // This is a simplified example
    }

    Ok(count <= limit)
}
}

Environment Variables Reference

VariableDescriptionDefault
CACHE_DRIVERCache backend ("memory" or "redis")memory
CACHE_PREFIXKey prefix for all entries-
CACHE_TTLDefault TTL in seconds3600
CACHE_MEMORY_CAPACITYMax entries for memory store10000
REDIS_URLRedis connection URLredis://127.0.0.1:6379

Best Practices

  1. Use meaningful cache keys - user:123:profile not key1
  2. Set appropriate TTLs - Balance freshness vs performance
  3. Use tags for related data - Makes invalidation easier
  4. Cache at the right level - Cache complete objects, not fragments
  5. Handle cache misses gracefully - Always have a fallback
  6. Use remember pattern - Cleaner code, less boilerplate
  7. Prefix keys in production - Avoid collisions between environments
  8. Monitor cache hit rates - Identify optimization opportunities

MCP Tools

Use cache_inspect to examine live cache state without writing debug code.

cache_inspect

Returns cached keys matching an optional prefix filter, along with their TTL, size, and tags. Use this to verify that values are being cached correctly, diagnose cache miss rates, or inspect which keys are tagged for bulk invalidation.