Skip to content

TTL & Lifecycle

Medha supports time-to-live (TTL) for cache entries. Entries past their TTL are invisible to search and eligible for removal by the background cleanup task.


Per-Entry TTL

Pass a ttl_seconds argument to store() to override the global default for a single entry:

async with Medha("demo", embedder=embedder, settings=settings) as cache:
    # This entry expires after 1 hour
    await cache.store(
        question="What is the current stock price of ACME?",
        generated_query="SELECT price FROM stocks WHERE symbol = 'ACME' ORDER BY ts DESC LIMIT 1",
        ttl_seconds=3600,
    )

    # This entry never expires (overrides a non-None global default)
    await cache.store(
        question="What is the schema of the users table?",
        generated_query="SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users'",
        ttl_seconds=None,
    )

Global Default TTL

Set default_ttl_seconds in Settings to apply a TTL to every entry that does not explicitly override it:

from medha import Settings

settings = Settings(
    backend_type="qdrant",
    default_ttl_seconds=86400,  # 24 hours
)

Entries stored without an explicit ttl_seconds will inherit this value. Set to None (the default) for no expiry.


Background Cleanup

When enable_background_cleanup=True (the default), Medha runs a background asyncio task that sweeps the vector backend for expired entries at a configurable interval:

settings = Settings(
    enable_background_cleanup=True,
    cleanup_interval_seconds=300,  # sweep every 5 minutes
)

The cleanup task starts when you enter the async context manager and stops when you exit it. It does not block searches.


Manual Expiry

Call expire() to immediately mark a specific entry as expired without waiting for the cleanup task:

async with Medha("demo", embedder=embedder, settings=settings) as cache:
    entry_id = await cache.store("How many users?", "SELECT COUNT(*) FROM users")

    # Immediately expire this entry
    await cache.expire(entry_id)

    # Now search returns None
    hit = await cache.search("How many users?")
    assert hit is None

Entry Lifecycle

gantt
    title Cache Entry Lifecycle
    dateFormat  s
    axisFormat  %S s

    section Entry
    Stored              : milestone, m1, 0, 0s
    Active (searchable) : active, 0, 60s
    TTL expires         : milestone, m2, 60, 0s
    Invisible to search : crit, 60, 90s
    Background cleanup  : milestone, m3, 90, 0s
    Deleted from backend: done, 90, 100s
Phase Description
Active Entry is stored and returned by searches
Expired TTL has passed; entry is hidden from search results but not yet deleted
Deleted Background cleanup has physically removed the entry from the backend

Note

The window between expiry and deletion depends on cleanup_interval_seconds. During this window, the entry counts as a miss in search, but storage is still consumed. Reduce the interval if storage efficiency is critical.