Skip to content
Back to Blog
1 min read

Time-To-Live (TTL) in Azure Cosmos DB: Automatic Data Expiration

I wrote “Time-To-Live (TTL) in Azure Cosmos DB: Automatic Data Expiration” to share practical, production-minded guidance on this topic.

Enabling TTL on a Container

# Enable TTL on container with default expiration
az cosmosdb sql container update \
    --resource-group myResourceGroup \
    --account-name mycosmosaccount \
    --database-name mydb \
    --name sessions \
    --ttl 3600  # Default 1 hour in seconds

# Enable TTL without default (per-item only)
az cosmosdb sql container update \
    --resource-group myResourceGroup \
    --account-name mycosmosaccount \
    --database-name mydb \
    --name mycontainer \
    --ttl -1  # No default, items must specify TTL

Using C#:

// Create container with TTL enabled
public async Task CreateContainerWithTTL()
{
    var containerProperties = new ContainerProperties
    {
        Id = "sessions",
        PartitionKeyPath = "/userId",
        DefaultTimeToLive = 3600  // 1 hour default
    };

    await _database.CreateContainerIfNotExistsAsync(containerProperties);
}

// Create container with TTL enabled but no default
public async Task CreateContainerWithItemLevelTTL()
{
    var containerProperties = new ContainerProperties
    {
        Id = "events",
        PartitionKeyPath = "/eventType",
        DefaultTimeToLive = -1  // Per-item TTL only
    };

    await _database.CreateContainerIfNotExistsAsync(containerProperties);
}

Setting TTL on Individual Items

// JavaScript - Creating items with TTL
const { CosmosClient } = require("@azure/cosmos");

class SessionManager {
    constructor(container) {
        this.container = container;
    }

    async createSession(userId, sessionData, durationMinutes = 60) {
        const session = {
            id: `session_${Date.now()}`,
            userId: userId,
            data: sessionData,
            createdAt: new Date().toISOString(),
            ttl: durationMinutes * 60  // TTL in seconds
        };

        const { resource } = await this.container.items.create(session);
        return resource;
    }

    async extendSession(sessionId, userId, additionalMinutes) {
        const { resource: session } = await this.container
            .item(sessionId, userId)
            .read();

        // Extend TTL by updating the item
        session.ttl = session.ttl + (additionalMinutes * 60);
        session.extendedAt = new Date().toISOString();

        const { resource: updated } = await this.container
            .item(sessionId, userId)
            .replace(session);

        return updated;
    }

    async createPersistentSession(userId, sessionData) {
        // TTL of -1 means never expire (overrides container default)
        const session = {
            id: `persistent_${Date.now()}`,
            userId: userId,
            data: sessionData,
            createdAt: new Date().toISOString(),
            ttl: -1
        };

        const { resource } = await this.container.items.create(session);
        return resource;
    }
}

TTL Patterns for Different Use Cases

# Python - Different TTL patterns
from azure.cosmos import CosmosClient
from datetime import datetime, timedelta

class TTLPatterns:
    def __init__(self, container):
        self.container = container

    def create_audit_log(self, action, user_id, details, retention_days=90):
        """Audit logs with configurable retention"""
        log = {
            'id': f"audit_{datetime.utcnow().timestamp()}",
            'partitionKey': f"{datetime.utcnow().strftime('%Y-%m')}",
            'action': action,
            'userId': user_id,
            'details': details,
            'timestamp': datetime.utcnow().isoformat(),
            'ttl': retention_days * 24 * 60 * 60
        }
        return self.container.create_item(log)

    def create_cache_entry(self, key, value, cache_seconds=300):
        """Short-lived cache entries"""
        entry = {
            'id': key,
            'partitionKey': 'cache',
            'value': value,
            'cachedAt': datetime.utcnow().isoformat(),
            'ttl': cache_seconds
        }
        return self.container.upsert_item(entry)

    def create_rate_limit_window(self, client_id, limit=100, window_seconds=60):
        """Rate limiting with TTL-based windows"""
        window = {
            'id': f"ratelimit_{client_id}_{int(datetime.utcnow().timestamp() / window_seconds)}",
            'partitionKey': client_id,
            'count': 1,
            'limit': limit,
            'windowStart': datetime.utcnow().isoformat(),
            'ttl': window_seconds * 2  # Keep slightly longer than window
        }
        return self.container.upsert_item(window)

    def create_scheduled_task(self, task_id, execute_at, task_data):
        """Tasks that auto-delete after execution window"""
        execution_time = datetime.fromisoformat(execute_at)
        now = datetime.utcnow()

        # Calculate TTL to expire 1 hour after scheduled execution
        ttl_seconds = int((execution_time - now).total_seconds()) + 3600

        task = {
            'id': task_id,
            'partitionKey': 'scheduled_tasks',
            'executeAt': execute_at,
            'taskData': task_data,
            'status': 'pending',
            'ttl': max(ttl_seconds, 3600)  # Minimum 1 hour
        }
        return self.container.create_item(task)

Monitoring TTL Deletions

// C# - Monitoring TTL using Change Feed
public class TTLMonitor
{
    private readonly Container _container;

    public async Task MonitorDeletions(CancellationToken cancellationToken)
    {
        var changeFeedProcessor = _container
            .GetChangeFeedProcessorBuilder<ExpandoObject>(
                "ttlMonitor",
                HandleChangesAsync)
            .WithInstanceName("instance1")
            .WithLeaseContainer(_leaseContainer)
            .Build();

        await changeFeedProcessor.StartAsync();
    }

    private async Task HandleChangesAsync(
        IReadOnlyCollection<ExpandoObject> changes,
        CancellationToken cancellationToken)
    {
        foreach (dynamic change in changes)
        {
            // Check for TTL deletion indicator
            if (change._deleted == true && change._ttlExpired == true)
            {
                Console.WriteLine($"Item {change.id} expired due to TTL");

                // Log or handle the expiration
                await LogExpiration(change.id, change._ts);
            }
        }
    }
}

TTL with Different Data Tiers

// TypeScript - Tiered data management with TTL
interface DataTierConfig {
    hot: number;    // TTL in seconds for hot data
    warm: number;   // TTL for warm data
    cold: number;   // TTL for cold data
}

class TieredDataManager {
    private container: Container;
    private tiers: DataTierConfig = {
        hot: 3600,         // 1 hour
        warm: 86400 * 7,   // 1 week
        cold: 86400 * 90   // 90 days
    };

    async createDataItem(data: any, tier: 'hot' | 'warm' | 'cold') {
        const item = {
            id: `${tier}_${Date.now()}`,
            partitionKey: data.category,
            tier: tier,
            data: data,
            createdAt: new Date().toISOString(),
            ttl: this.tiers[tier]
        };

        return await this.container.items.create(item);
    }

    async promoteToWarmer(itemId: string, partitionKey: string) {
        const { resource: item } = await this.container
            .item(itemId, partitionKey)
            .read();

        // Upgrade tier and extend TTL
        if (item.tier === 'cold') {
            item.tier = 'warm';
            item.ttl = this.tiers.warm;
        } else if (item.tier === 'warm') {
            item.tier = 'hot';
            item.ttl = this.tiers.hot;
        }

        item.promotedAt = new Date().toISOString();

        return await this.container
            .item(itemId, partitionKey)
            .replace(item);
    }

    async archiveItem(itemId: string, partitionKey: string) {
        const { resource: item } = await this.container
            .item(itemId, partitionKey)
            .read();

        // Set TTL to -1 for permanent storage
        item.tier = 'archived';
        item.ttl = -1;
        item.archivedAt = new Date().toISOString();

        return await this.container
            .item(itemId, partitionKey)
            .replace(item);
    }
}

Best Practices

  1. Plan TTL values carefully: Consider business requirements
  2. Use -1 for important data: Prevent accidental deletion
  3. Monitor via Change Feed: Track TTL deletions for audit
  4. Test expiration timing: TTL isn’t instantaneous
  5. Consider RU impact: Deletions consume throughput

TTL is a powerful feature for automatic data lifecycle management, reducing storage costs and ensuring compliance with data retention policies without manual intervention.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.