Una caché evita trabajo costoso repetido, pero introduce datos posiblemente desactualizados. cachetools ofrece políticas limitadas, entre ellas TTLCache, que combina expiración y descarte LRU.

Instalar y definir el contrato

python -m pip install cachetools

Antes del código, define la operación costosa, la antigüedad aceptable, el presupuesto de memoria y el evento de invalidación. La caché solo es segura si quien llama tolera un valor antiguo durante el TTL. Autorización, saldos y reservas de inventario pueden exigir invalidación explícita o ninguna caché.

from cachetools import TTLCache, cached
from threading import RLock

cache = TTLCache(maxsize=500, ttl=60)
lock = RLock()


@cached(cache=cache, lock=lock)
def obtener_producto(producto_id: int) -> dict[str, object]:
    return consultar_base(producto_id)

maxsize cuenta entradas por defecto, no bytes. Quinientos documentos grandes no equivalen a quinientos enteros. Pasa getsizeof si el tamaño representa mejor la capacidad, recordando que se calcula al insertar. Mutar el objeto después hace inexacta la cuenta. Valores inmutables o copias defensivas evitan cambios silenciosos.

Diseñar claves completas

Todo dato que cambie el resultado debe entrar en la clave: idioma, tenant, permisos, versión de reglas y formato. Normaliza entradas equivalentes. Nunca mezcles respuestas privadas. Mantén claves hashable y pequeñas, sin tokens, contraseñas ni datos personales brutos.

from cachetools import TTLCache, cachedmethod
from cachetools.keys import hashkey
from operator import attrgetter


class Catalogo:
    def __init__(self) -> None:
        self.cache = TTLCache(maxsize=200, ttl=120)

    @cachedmethod(
        attrgetter("cache"),
        key=lambda self, producto_id, idioma: hashkey(producto_id, idioma.lower()),
    )
    def cargar(self, producto_id: int, idioma: str) -> dict[str, object]:
        return consultar_catalogo(producto_id, idioma)

Entender expiración y expulsión

El TTL usa un reloj monotónico por defecto, evitando problemas si cambia el reloj civil. Los elementos vencidos no se devuelven, pero el espacio puede recuperarse en una escritura posterior o con expire(). En un proceso con pocas escrituras, un mantenimiento deliberado ayuda a controlar memoria.

Al alcanzar maxsize, TTLCache retira vencidos primero y después aplica LRU. El TTL no garantiza permanencia durante todo el plazo: la capacidad puede expulsar antes. La expiración no es limpieza activa en segundo plano. El código debe funcionar ante cualquier miss.

retirados = cache.expire()
for clave, valor in retirados:
    liberar_recurso(valor)

No dependas de la expulsión para acciones esenciales. Un reinicio puede omitirla.

Controlar concurrencia y avalanchas

El lock protege el acceso, pero la función decorada puede ejecutarse fuera de esa sección crítica. Algunas versiones aceptan una condición para que llamadas concurrentes de la misma clave esperen un cálculo. Comprueba la documentación correspondiente y pruébalo.

Para un origen lento, usa también timeout y concurrencia limitada. TTL corto y tráfico sincronizado pueden causar muchos misses juntos. Considera jitter acotado, actualización anticipada o caché externo con coordinación distribuida. No sirvas datos antiguos indefinidamente para ocultar una caída.

Invalidar deliberadamente

El tiempo es solo una estrategia. Tras actualizar un producto, retira su clave o incrementa una versión incluida en ella. Los namespaces versionados ayudan cuando muchas entradas dependen de una regla. cache.clear() es sencillo, pero puede producir un pico en la base.

La caché negativa puede guardar “no encontrado” brevemente. Usa TTL menor y distingue ausencia de fallo transitorio. Timeout, permiso denegado y registro ausente requieren tratamientos distintos.

Probar sin esperar

class Reloj:
    ahora = 0.0

    def __call__(self) -> float:
        return self.ahora


reloj = Reloj()
cache_prueba = TTLCache(maxsize=2, ttl=10, timer=reloj)
cache_prueba["a"] = 1
reloj.ahora = 11
assert "a" not in cache_prueba

Prueba aciertos, misses, expiración, capacidad, invalidación, aislamiento y concurrencia. Mide tasa de aciertos junto con latencia y errores del origen. Una tasa alta no sirve si los valores están obsoletos; una baja puede indicar que la complejidad no compensa.

El lock protege hilos, pero no distribuye valores entre procesos. Incluye en la clave todo lo que cambia el resultado, como usuario, idioma o versión de reglas. Nunca mezcles respuestas privadas.

La caché en memoria desaparece al reiniciar y diverge entre workers. Usa un servicio externo si varias instancias necesitan una visión compartida. Mide aciertos, fallos, latencia y tamaño con la guía de Prometheus Client.

La caché local sirve para resultados descartables en un proceso. Elige una externa si los workers necesitan invalidación compartida, límites coordinados o valores que sobrevivan al despliegue. Incluso entonces, conserva la base o API oficial como fuente de verdad y diseña para la pérdida completa de la caché.

Revisar modos de fallo

Simula una caché vacía al arrancar, una caché llena bajo carga, el origen no disponible y dos solicitudes para la misma clave fría. Comprueba que un miss nunca afecta la corrección y que la memoria sigue acotada. Si los valores poseen archivos, sockets u otros recursos, gestiónalos fuera de la caché en vez de confiar en que la expulsión siempre los cerrará.

Documenta la unidad del TTL junto a la configuración y evita números mágicos. Una elección útil considera frecuencia de cambio, coste del miss y antigüedad tolerada. Revísala cuando cambien tráfico o reglas.

No expongas el objeto mutable como diccionario general. Encapsúlalo tras una interfaz pequeña que controle claves, invalidación y métricas. Así los consumidores no crean claves incompatibles ni eluden sincronización, y reemplazar la implementación resulta más sencillo.

Revisa también la mutabilidad cuando el valor cruza una API. Entregar el mismo diccionario a varios consumidores puede acoplar solicitudes sin intención. Convierte objetos de dominio en una representación estable en la frontera y almacena solo la capa cuya responsabilidad sea clara. Documenta si el retorno se puede modificar y devuelve una copia cuando proceda.

Un despliegue gradual puede ejecutar versiones de código con formatos distintos. Incluye una versión de esquema en la clave si ambas conviven, o limpia el espacio de nombres durante una migración controlada. Nunca deserialices datos de caché no confiables con mecanismos capaces de ejecutar código. Valida tipos y trata cualquier valor como prescindible.

La documentación oficial de cachetools, consultada el 22 de julio de 2026, explica LRU, TTL, claves y sincronización. La caché es una optimización descartable; la base de datos sigue siendo la fuente de verdad.