Redis TTL and Expiration Strategy
The system implements sophisticated TTL (Time-To-Live) strategies to manage data lifecycle, memory usage, and cache consistency across different operational requirements.
TTL Strategy Overview
Multi-Tier Expiration Strategy
Data Lifecycle Management
├── Ephemeral Data (seconds-minutes)
│ ├── Inventory reservations (15 min)
│ ├── Checkout locks (10 min)
│ └── Session tokens (30 min)
├── Operational Data (minutes-hours)
│ ├── Checkout sessions (1 hour)
│ ├── Payment intents (24 hours)
│ └── Idempotency keys (24 hours)
├── Business Data (hours-days)
│ ├── Product details (24 hours)
│ ├── Discount rules (24 hours)
│ └── Bundle definitions (24 hours)
└── User Data (days-weeks)
├── Shopping carts (30 days)
├── User preferences (30 days)
└── Search history (7 days)
TTL Categories by Purpose
1. Operational Safety
- Purpose: Prevent race conditions and ensure data consistency
- TTL Range: Seconds to minutes
- Examples: Locks, reservations, temporary state
2. Business Continuity
- Purpose: Maintain operational state across reasonable timeframes
- TTL Range: Minutes to hours
- Examples: Sessions, payment intents, idempotency
3. Performance Optimization
- Purpose: Cache frequently accessed data
- TTL Range: Hours to days
- Examples: Product data, computed results, search indexes
4. User Experience
- Purpose: Preserve user state and preferences
- TTL Range: Days to weeks
- Examples: Carts, preferences, browsing history
TTL Configuration
Centralized TTL Management
export const TTL = {
// Operational safety (seconds to minutes)
INVENTORY_RESERVATION: 15 * 60, // 15 minutes - cart reservation timeout
CHECKOUT_LOCK: 10 * 60, // 10 minutes - prevent concurrent checkout
RATE_LIMIT_WINDOW: 60, // 1 minute - rate limiting window
SESSION_TOKEN: 30 * 60, // 30 minutes - session token validity
// Business continuity (minutes to hours)
CHECKOUT_SESSION: 60 * 60, // 1 hour - checkout session timeout
PAYMENT_INTENT: 24 * 60 * 60, // 24 hours - payment intent validity
IDEMPOTENCY_KEY: 24 * 60 * 60, // 24 hours - prevent duplicate operations
TEMPORARY_UPLOAD: 2 * 60 * 60, // 2 hours - temporary file uploads
// Performance optimization (hours to days)
PRODUCT_DETAILS: 24 * 60 * 60, // 24 hours - product information
DISCOUNT_RULES: 24 * 60 * 60, // 24 hours - discount configurations
BUNDLE_DEFINITION: 24 * 60 * 60, // 24 hours - bundle structures
SEARCH_RESULTS: 4 * 60 * 60, // 4 hours - search result caching
USER_PREFERENCES: 7 * 24 * 60 * 60, // 7 days - user preferences
// User experience (days to weeks)
SHOPPING_CART: 30 * 24 * 60 * 60, // 30 days - cart persistence
BROWSE_HISTORY: 7 * 24 * 60 * 60, // 7 days - browsing history
RECOMMENDATIONS: 3 * 24 * 60 * 60, // 3 days - personalized recommendations
AB_TEST_ASSIGNMENTS: 30 * 24 * 60 * 60, // 30 days - A/B test assignments
} as const;
Dynamic TTL Calculation
class DynamicTTLManager {
// Adjust TTL based on data volatility
calculateTTL(dataType: string, accessFrequency: number): number {
const baseTTL = TTL[dataType as keyof typeof TTL];
// Increase TTL for frequently accessed data
if (accessFrequency > 100) {
return baseTTL * 2;
}
// Decrease TTL for rarely accessed data
if (accessFrequency < 10) {
return Math.max(baseTTL / 4, 300); // Minimum 5 minutes
}
return baseTTL;
}
// Contextual TTL based on user behavior
calculateUserTTL(userId: string, dataType: string): number {
const userActivity = this.getUserActivityLevel(userId);
const baseTTL = TTL[dataType as keyof typeof TTL];
switch (userActivity) {
case 'high':
return baseTTL * 3; // Very active users get longer TTL
case 'medium':
return baseTTL; // Normal TTL
case 'low':
return Math.max(baseTTL / 2, 300); // Less active users get shorter TTL
default:
return baseTTL;
}
}
}
Expiration Event Handling
Proactive Expiration Management
class ExpirationManager {
private expirationEvents = new Map<string, ExpirationHandler>();
// Register expiration handlers
onExpire(keyPattern: string, handler: ExpirationHandler): void {
this.expirationEvents.set(keyPattern, handler);
}
// Handle expiration events from Redis keyspace notifications
async handleExpirationEvent(key: string): Promise<void> {
for (const [pattern, handler] of this.expirationEvents) {
if (this.matchesPattern(key, pattern)) {
await handler(key);
break;
}
}
}
private matchesPattern(key: string, pattern: string): boolean {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(key);
}
}
// Usage
const expirationManager = new ExpirationManager();
// Handle cart expiration
expirationManager.onExpire('cart:customer:*', async (key) => {
const customerId = key.split(':')[2];
await handleExpiredCart(customerId);
});
// Handle reservation expiration
expirationManager.onExpire('inventory:reservation:*:*', async (key) => {
const [, , cartId, variantId] = key.split(':');
await releaseExpiredReservation(cartId, variantId);
});
Expiration Cleanup Jobs
class ExpirationCleanupService {
@Cron('0 */6 * * *') // Every 6 hours
async cleanupExpiredData(): Promise<void> {
// Clean up orphaned cart data
await this.cleanupOrphanedCarts();
// Clean up expired payment intents
await this.cleanupExpiredPaymentIntents();
// Clean up stale search indexes
await this.cleanupStaleSearchIndexes();
// Update expiration metrics
await this.updateExpirationMetrics();
}
private async cleanupOrphanedCarts(): Promise<void> {
const orphanedKeys = await redis.keys('cart:session:*');
for (const key of orphanedKeys) {
const ttl = await redis.ttl(key);
if (ttl === -2) { // Key doesn't exist (expired)
// Clean up related data
const sessionId = key.split(':')[2];
await this.cleanupSessionData(sessionId);
}
}
}
private async cleanupExpiredPaymentIntents(): Promise<void> {
const expiredIntents = await db.query.paymentIntents.findMany({
where: sql`created_at < ${new Date(Date.now() - TTL.PAYMENT_INTENT * 1000)}`,
where: { status: 'pending' }
});
for (const intent of expiredIntents) {
await paymentService.cancelExpiredIntent(intent.id);
}
}
}
TTL Extension Strategies
Sliding Expiration
class SlidingExpirationManager {
// Extend TTL on access for frequently used data
async getWithSlidingExpiration(key: string, extensionSeconds: number): Promise<any> {
const data = await redis.get(key);
if (data) {
// Extend expiration on access
const currentTTL = await redis.ttl(key);
if (currentTTL > 0 && currentTTL < extensionSeconds) {
await redis.expire(key, extensionSeconds);
}
}
return data ? JSON.parse(data) : null;
}
// Conditional extension based on access patterns
async smartExtendTTL(key: string): Promise<void> {
const accessCount = await this.getAccessCount(key);
if (accessCount > 10) {
// Frequently accessed, extend TTL
await redis.expire(key, TTL.PRODUCT_DETAILS * 2);
} else if (accessCount < 3) {
// Rarely accessed, let it expire sooner
const currentTTL = await redis.ttl(key);
if (currentTTL > 3600) { // More than 1 hour left
await redis.expire(key, 3600); // Set to 1 hour
}
}
}
private async getAccessCount(key: string): Promise<number> {
const accessKey = `${key}:access_count`;
const count = await redis.get(accessKey);
await redis.incr(accessKey);
await redis.expire(accessKey, 3600); // Reset access count every hour
return parseInt(count || '0');
}
}
Graceful Expiration
class GracefulExpirationManager {
private gracePeriod = 300; // 5 minutes grace period
// Check if key is in grace period before expiration
async getWithGracePeriod(key: string): Promise<any> {
const data = await redis.get(key);
const ttl = await redis.ttl(key);
if (data && ttl > 0) {
// Check if in grace period
if (ttl <= this.gracePeriod) {
// Extend slightly to give background refresh time
await redis.expire(key, ttl + 60);
// Trigger background refresh
this.triggerBackgroundRefresh(key);
}
return JSON.parse(data);
}
return null;
}
private async triggerBackgroundRefresh(key: string): Promise<void> {
// Use Redis pub/sub to trigger refresh in another process
await redis.publish('cache-refresh', JSON.stringify({
key,
priority: 'high',
reason: 'graceful-expiration'
}));
}
}
Expiration Monitoring
TTL Metrics Collection
class TTLMonitoringService {
async collectTTLStats(): Promise<TTLStats> {
const keys = await redis.keys('*');
const ttlStats = new Map<string, number[]>();
for (const key of keys) {
const ttl = await redis.ttl(key);
if (ttl > 0) {
const keyType = key.split(':')[0];
if (!ttlStats.has(keyType)) {
ttlStats.set(keyType, []);
}
ttlStats.get(keyType)!.push(ttl);
}
}
return {
totalKeys: keys.length,
keysByType: Object.fromEntries(
Array.from(ttlStats.entries()).map(([type, ttls]) => [
type,
{
count: ttls.length,
avgTTL: ttls.reduce((a, b) => a + b, 0) / ttls.length,
minTTL: Math.min(...ttls),
maxTTL: Math.max(...ttls),
}
])
),
};
}
// Monitor expiration rates
@Cron('0 * * * *') // Every hour
async monitorExpirationRates(): Promise<void> {
const beforeCount = await redis.dbsize();
await new Promise(resolve => setTimeout(resolve, 1000));
const afterCount = await redis.dbsize();
const expiredCount = beforeCount - afterCount;
metrics.gauge('redis_expired_keys_per_hour', expiredCount);
}
}
Expiration Alerts
class ExpirationAlertManager {
private readonly thresholds = {
cart: { warning: 1000, critical: 100 },
inventory: { warning: 10, critical: 1 },
checkout: { warning: 50, critical: 10 },
};
async checkExpirationThresholds(): Promise<void> {
for (const [type, threshold] of Object.entries(this.thresholds)) {
const pattern = `${type}:*`;
const keys = await redis.keys(pattern);
const expiringSoon = [];
for (const key of keys) {
const ttl = await redis.ttl(key);
if (ttl > 0 && ttl <= threshold.warning) {
expiringSoon.push({ key, ttl });
}
}
if (expiringSoon.length >= threshold.critical) {
await this.sendCriticalAlert(type, expiringSoon.length);
} else if (expiringSoon.length >= threshold.warning) {
await this.sendWarningAlert(type, expiringSoon.length);
}
}
}
private async sendCriticalAlert(type: string, count: number): Promise<void> {
await alertService.send({
severity: 'critical',
title: `Critical: High ${type} expiration rate`,
message: `${count} ${type} keys expiring within threshold`,
channel: 'engineering',
});
}
private async sendWarningAlert(type: string, count: number): Promise<void> {
await alertService.send({
severity: 'warning',
title: `Warning: ${type} expiration rate`,
message: `${count} ${type} keys expiring soon`,
channel: 'engineering',
});
}
}
Memory Management
TTL-Based Eviction
class MemoryManager {
// Evict expired keys proactively
async evictExpiredKeys(): Promise<void> {
// Use Redis SCAN to find keys with TTL
let cursor = '0';
const expiredKeys = [];
do {
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', '*', 'COUNT', 1000);
for (const key of keys) {
const ttl = await redis.ttl(key);
if (ttl === -2) { // Key doesn't exist
expiredKeys.push(key);
}
}
cursor = newCursor;
} while (cursor !== '0');
if (expiredKeys.length > 0) {
await redis.del(...expiredKeys);
logger.info(`Evicted ${expiredKeys.length} expired keys`);
}
}
// Memory-aware TTL adjustment
async adjustTTLForMemoryPressure(): Promise<void> {
const memoryUsage = await this.getMemoryUsage();
if (memoryUsage > 0.8) { // 80% memory usage
// Reduce TTL for less critical data
await this.reduceTTLs(['search', 'recommendations'], 0.5);
} else if (memoryUsage > 0.9) { // 90% memory usage
// Aggressive TTL reduction
await this.reduceTTLs(['product', 'bundle', 'discount'], 0.25);
}
}
private async reduceTTLs(keyTypes: string[], factor: number): Promise<void> {
for (const type of keyTypes) {
const keys = await redis.keys(`${type}:*`);
for (const key of keys) {
const currentTTL = await redis.ttl(key);
if (currentTTL > 0) {
const newTTL = Math.max(Math.floor(currentTTL * factor), 300); // Min 5 minutes
await redis.expire(key, newTTL);
}
}
}
}
private async getMemoryUsage(): Promise<number> {
const info = await redis.info('memory');
const usedMemory = parseInt(info.match(/used_memory:(\d+)/)?.[1] || '0');
const maxMemory = parseInt(info.match(/maxmemory:(\d+)/)?.[1] || '0');
return maxMemory > 0 ? usedMemory / maxMemory : 0;
}
}
Expiration Patterns
Business Logic Expiration
class BusinessLogicExpiration {
// Expire cart when order is placed
async expireCartOnOrder(orderId: string, customerId: string): Promise<void> {
const cartKey = KEY_PATTERNS.CART_CUSTOMER(customerId);
// Check if cart exists and expire it
const exists = await redis.exists(cartKey);
if (exists) {
await redis.del(cartKey);
logger.info(`Expired cart for customer ${customerId} after order ${orderId}`);
}
}
// Expire payment intent after successful payment
async expirePaymentIntentAfterPayment(paymentIntentId: string): Promise<void> {
const intentKey = KEY_PATTERNS.PAYMENT_INTENT_BY_ID(paymentIntentId);
// Set short TTL instead of immediate deletion to allow for webhooks
await redis.expire(intentKey, 300); // 5 minutes grace period
}
// Expire idempotency keys after operation completes
async expireIdempotencyKey(operation: string, key: string): Promise<void> {
const idempotencyKey = KEY_PATTERNS.IDEMPOTENCY(operation, key);
// Keep for full TTL to prevent duplicate operations
// Will be cleaned up by TTL naturally
logger.debug(`Idempotency key ${idempotencyKey} will expire in ${TTL.IDEMPOTENCY} seconds`);
}
}
Cascading Expiration
class CascadingExpirationManager {
private readonly cascades = new Map<string, string[]>([
['product', ['product:variants', 'product:reviews', 'product:search']],
['customer', ['cart:customer', 'preferences', 'recommendations']],
['checkout', ['checkout:metadata', 'payment:intent', 'checkout:lock']],
]);
async cascadeExpire(primaryType: string, id: string): Promise<void> {
const cascadeTypes = this.cascades.get(primaryType) || [];
const deletePromises = cascadeTypes.map(async (type) => {
const pattern = `${type}:${id}*`;
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
logger.info(`Cascading expiration: deleted ${keys.length} keys matching ${pattern}`);
}
});
await Promise.all(deletePromises);
}
// Register cascade relationships
registerCascade(primaryType: string, dependentTypes: string[]): void {
this.cascades.set(primaryType, dependentTypes);
}
}
Best Practices
TTL Design Principles
- Business-Aligned TTL: Set TTL based on business requirements, not technical defaults
- User Experience Focus: Longer TTL for user-facing features, shorter for internal operations
- Memory Awareness: Adjust TTL based on memory pressure and access patterns
- Monitoring First: Monitor expiration patterns before setting aggressive TTL
Operational Excellence
- Expiration Monitoring: Track expiration rates and patterns
- Graceful Handling: Implement grace periods and background refresh
- Cleanup Automation: Automated cleanup of expired and orphaned data
- Alert Thresholds: Configure appropriate alerting for expiration anomalies
Performance Optimization
- Proactive Expiration: Use keyspace notifications for immediate cleanup
- Batch Operations: Batch expiration operations to reduce Redis load
- Smart Extension: Extend TTL intelligently based on access patterns
- Memory Management: Adjust TTL dynamically based on memory usage
Reliability
- Graceful Degradation: Handle expiration events without service disruption
- Data Consistency: Ensure expired data doesn't cause inconsistencies
- Audit Trail: Log expiration events for debugging and compliance
- Recovery Mechanisms: Implement recovery procedures for accidental expiration