← Back to Blog

Cache TTL Best Practices: How Long Should You Cache Data?

December 22, 2025 • 7 min read • Caching Strategy

Setting cache TTL (Time To Live) is one of the most important—and most overlooked—caching decisions. Too short and you lose performance benefits. Too long and users see stale data. Here's how to get it right.

The TTL Decision Framework

Every TTL decision balances three factors:

  1. Data freshness requirements: How stale is acceptable?
  2. Access frequency: How often is this data requested?
  3. Change frequency: How often does this data change?

The ideal TTL is long enough to maximize cache hits, but short enough that stale data doesn't cause problems.

TTL Recommendations by Data Type

Data Type Recommended TTL Reasoning
User sessions 15-30 minutes Security + activity timeout
API rate limits 1-60 seconds Must be accurate
Product catalog 5-15 minutes Changes infrequently
User profiles 5-10 minutes Medium change frequency
Search results 1-5 minutes Personalization needs
Static config 1-24 hours Rarely changes
Feature flags 30-60 seconds Need fast propagation

Pattern 1: Short TTL + Active Invalidation

For data that changes unpredictably, use short TTLs as a safety net, but actively invalidate on changes:

// Set cache with short TTL
await cache.set(`user:${userId}`, userData, { ttl: 300 }); // 5 min

// But invalidate immediately on updates
async function updateUser(userId, updates) {
    await db.update('users', userId, updates);
    await cache.delete(`user:${userId}`);  // Active invalidation
}
Why both? The short TTL handles edge cases where invalidation fails. The active invalidation ensures users see updates immediately.

Pattern 2: Long TTL + Cache Warming

For stable data, use longer TTLs but pre-warm the cache to avoid cold starts:

// Long TTL for stable data
await cache.set('site:config', config, { ttl: 86400 }); // 24 hours

// Warm cache on deployment
async function warmConfigCache() {
    const config = await db.query('SELECT * FROM site_config');
    await cache.set('site:config', config, { ttl: 86400 });
}

// Also refresh periodically in background
setInterval(warmConfigCache, 3600000); // Every hour

Pattern 3: Sliding Expiration

For session-like data, reset TTL on each access:

async function getSession(sessionId) {
    const session = await cache.get(`session:${sessionId}`);

    if (session) {
        // Extend TTL on access
        await cache.expire(`session:${sessionId}`, 1800); // Reset to 30 min
    }

    return session;
}

This keeps active sessions alive while letting inactive ones expire.

Pattern 4: Stale-While-Revalidate

Serve stale data immediately while refreshing in the background:

async function getWithSWR(key, fetchFn, { ttl, staleTTL }) {
    const cached = await cache.get(key);
    const metadata = await cache.get(`${key}:meta`);

    if (cached) {
        const age = Date.now() - metadata.cachedAt;

        if (age > ttl * 1000) {
            // Data is stale - refresh in background
            refreshInBackground(key, fetchFn, ttl, staleTTL);
        }

        // Return cached data immediately
        return cached;
    }

    // No cache - fetch and store
    const data = await fetchFn();
    await cache.set(key, data, { ttl: staleTTL });
    await cache.set(`${key}:meta`, { cachedAt: Date.now() });
    return data;
}

Common TTL Mistakes

  1. Same TTL for everything: Different data has different freshness needs
  2. Forgetting thundering herd: When TTL expires, many requests hit the database simultaneously
  3. No TTL at all: Memory fills up with stale data
  4. Extremely long TTLs without invalidation: Users see outdated data for hours

Dynamic TTL Based on Access Patterns

The smartest approach: adjust TTL based on how the data is actually used:

function calculateDynamicTTL(key, accessHistory) {
    const avgTimeBetweenAccess = calculateAverage(accessHistory);

    // TTL should be 2-3x the access interval
    // Popular data gets longer TTL, rare data shorter
    const dynamicTTL = Math.min(
        avgTimeBetweenAccess * 2.5,
        86400  // Max 24 hours
    );

    return Math.max(dynamicTTL, 60); // Min 1 minute
}

This ensures frequently accessed data stays cached while rarely used data doesn't waste memory.

Let AI optimize your cache TTLs

Cachee.ai automatically adjusts TTLs based on real access patterns—no manual tuning required.

Start Free Trial