¿Qué hace que una clave de caché sea buena, y qué sale mal cuando el diseño de la clave falla (colisiones, falta de versión o de scoping por tenant)?
Una clave de caché es una promesa de que 'esta entrada exacta produce esta salida exacta'; rompe esa promesa y terminarás sirviendo los datos de otra persona, o de otro tenant, sin que aparezca un solo error en los logs.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué hace que una clave de caché sea buena, y qué sale mal cuando el diseño de la clave falla (colisiones, falta de versión o de scoping por tenant)?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Qué es en realidad una clave de caché
Una clave de caché es la identidad de un valor cacheado: lo que dice "esta entrada exacta produjo esta salida exacta, y puedes reutilizarla mientras yo siga siendo válida". Una buena clave de caché tiene tres propiedades: es determinista (la misma solicitud lógica siempre produce la misma clave), es completa (cada entrada que podría cambiar la salida queda reflejada en la clave), y está delimitada por namespace (nunca puede chocar con una clave generada para otro propósito, otro tenant o otra forma de dato).
Falla en cualquiera de esas tres y la caché no se vuelve simplemente más lenta: se vuelve incorrecta. Una caché lenta es un bug de rendimiento. Una caché con una mala estrategia de claves es un bug de corrección, y los bugs de corrección en el cacheo son especialmente peligrosos porque no parecen bugs: la solicitud tiene éxito, devuelve una respuesta que parece plausible, y nadie se da cuenta hasta que un cliente reporta estar viendo los datos de otra persona.
#Anatomía de una buena clave
La mayoría de las claves de caché de nivel producción se construyen con el mismo puñado de componentes, en capas de lo más amplio a lo más específico:
| Componente | Propósito | Fragmento de ejemplo |
|---|---|---|
| Namespace/prefijo | Agrupa claves relacionadas y evita que funcionalidades no relacionadas choquen entre sí | orders: |
| Versión | Permite invalidar toda una generación de claves tras un cambio de esquema o de forma, solo incrementando un número | v3: |
| Id de tenant/organización | Evita que los datos cacheados de un tenant se sirvan alguna vez a otro, en un sistema multi-tenant | tenant:acme-corp: |
| Entidad + identificador | Nombra el recurso exacto que describe el valor | customer:42: |
| Variante/parámetros | Cada filtro, orden, número de página o locale que cambia la salida | status:active:page:1:sort:created_at_desc:locale:es |
#Qué sale mal: parámetros faltantes
El bug más común es una clave a la que le falta una de las entradas que realmente afecta la consulta. Aquí hay un constructor de claves que parece razonable pero ignora silenciosamente la mitad de los argumentos de su propia función:
# MAL: la clave solo codifica customer_id, no status, page ni sortdef cache_key_for_orders(customer_id: int) -> str:return f"orders:{customer_id}"def get_orders(customer_id, status, page, sort):key = cache_key_for_orders(customer_id)cached = cache.get(key)if cached:return json.loads(cached)# ... ejecutar la consulta real con status/page/sort ...
Llama a get_orders(42, status="active", page=1, sort="date") y se cachea bajo orders:42. Llama a get_orders(42, status="cancelled", page=1, sort="date") dos segundos después, y lee de vuelta las órdenes activas bajo la misma clave — una colisión entre dos consultas lógicamente distintas que comparten un id de cliente. No es un caso extremo raro; es lo que pasa la primera vez que alguien agrega un nuevo filtro a un endpoint existente y olvida que la clave de caché también necesita actualizarse.
La solución es incorporar a la clave cada parámetro que cambia la salida, ya sea concatenándolos directamente o, cuando hay más de dos o tres, aplicándoles un hash conjunto para que la clave mantenga un largo manejable:
import hashlibimport jsondef cache_key(namespace: str, tenant_id: str, version: str = "v3", **params) -> str:payload = json.dumps(params, sort_keys=True)digest = hashlib.sha256(payload.encode()).hexdigest()[:16]return f"{namespace}:{version}:tenant:{tenant_id}:{digest}"# cache_key("orders", "acme-corp", customer_id=42, status="active", page=1, sort="date")# -> "orders:v3:tenant:acme-corp:7f3a9c1d4e2b8081"
Dos detalles importan aquí. Primero, sort_keys=True hace que el payload JSON sea determinista sin importar el orden en que se pasaron los argumentos — sin eso, {"a": 1, "b": 2} y {"b": 2, "a": 1} producirían hashes distintos para lo que es, lógicamente, la misma solicitud. Segundo, el digest se trunca a 16 caracteres hexadecimales (64 bits) en lugar de 4 u 8: truncar demasiado agresivamente hace que la paradoja del cumpleaños vuelva probables las colisiones accidentales apenas tengas del orden de decenas de miles de claves vivas, lo que convierte "consulta distinta, misma clave" de un bug que tú introduces en un bug que ocurre solo.
#Qué sale mal: falta de scoping por tenant
En un sistema multi-tenant, omitir el id del tenant en la clave es el mismo tipo de bug que omitir un parámetro de filtro — salvo que el radio de impacto es una fuga de datos en vez de una página de resultados equivocada.
# PELIGROSO: sin ningún scoping por tenantdef cache_key_missing_tenant(customer_id: int, status: str) -> str:return f"orders:{customer_id}:{status}"
Si los ids de cliente son únicos solo dentro de un tenant (ids autoincrementales por tenant, o ids reutilizados entre shards), entonces customer_id=42 en el Tenant A y customer_id=42 en el Tenant B producen exactamente la misma clave. La solicitud del tenant que llegue primero puebla la caché, y la solicitud del otro tenant la lee de vuelta — los datos de órdenes de una empresa servidos a una empresa completamente distinta, sin error, sin excepción, sin ninguna línea de log que parezca anormal. Esta es una de las causas más comunes en el mundo real de fugas de datos entre tenants en productos SaaS, y rara vez la detectan las pruebas, porque un conjunto de tests de un solo tenant no tiene forma de ejercitarla.
#Qué sale mal: falta de versión
El último fallo común aparece después de un despliegue, no en el momento de la solicitud. Si una clave de caché no incorpora una versión, y un despliegue cambia la forma del valor cacheado —un campo renombrado, un campo nuevo obligatorio, un formato de serialización distinto—, las entradas de caché antiguas escritas por la versión anterior siguen ahí, y el código nuevo, o falla al intentar deserializarlas, o lee silenciosamente un None/null donde esperaba un valor. Incorporar una versión en la clave (v3: en los ejemplos anteriores) convierte eso en un no-evento: el código nuevo simplemente nunca pide las claves viejas, y estas expiran solas de forma natural.