¿Por qué usar caché?
Una caché es una copia de datos almacenada en un lugar más accesible y rápido que la fuente original. Cada decisión de implementar caché es, en realidad, una apuesta: que el mismo valor será solicitado nuevamente antes de que cambie, y que servir una copia ligeramente desactualizada es aceptable. Cuando ambas condiciones se cumplen, la recompensa es enorme.
Un solo acierto (hit) genera tres efectos:
- Latencia. Una consulta en Redis responde en mucho menos de un milisegundo; la misma fila en PostgreSQL requiere varios milisegundos de red, planificación y E/S. En una página que lee veinte elementos, esa diferencia define toda la experiencia.
- Carga. Si el 95% de las lecturas se sirven desde la caché, la base de datos procesa una sola consulta donde antes procesaba veinte. Esto permite sobrevivir a picos de tráfico o ejecutar una instancia más pequeña sin cambiar una sola línea de la lógica de las consultas.
- Coste. Menos lecturas de base de datos significan instancias más pequeñas, menos réplicas de lectura y menos tráfico entre regiones. A escala, el almacenamiento en caché es una de las pocas optimizaciones que se paga sola en términos monetarios.
El precio a pagar es la complejidad. Una caché es una segunda copia de la verdad, y cada copia puede entrar en conflicto. La mayor parte de esta guía trata sobre cómo mantener ese conflicto al mínimo y de forma temporal.
Dónde puede residir una caché
El almacenamiento en caché no es un único sistema, sino un stack de capas; cada una está más cerca del lector y cada una tiene una lógica de invalidación diferente.
- El navegador. Las respuestas HTTP marcadas como
Cache-Control: max-agese reutilizan sin realizar una petición. Esto es gratuito, privado y está mayormente bajo el control del cliente. - El CDN o edge. Una caché compartida delante de tu origen absorbe el tráfico de las respuestas públicas. Es rápida y global, pero un error aquí es visible para todo el mundo.
- La aplicación. Una caché en proceso (un
Map, un LRU) es el nivel más rápido porque evita la red por completo. También es por instancia, por lo que debe ser pequeña y desechable. - Un almacén compartido. Redis o Memcached se sitúan junto a la aplicación y sirven a cada instancia desde una copia consistente. Aquí es donde reside la mayor parte de la caché de aplicación.
- La base de datos. Los buffer pools, las vistas materializadas, los planes preparados y las cachés de resultados de consultas mantienen la fuente rápida por sí misma. Son la última línea antes del disco.
Una petición puede ser respondida en cualquiera de estas capas. Cuanto más cerca esté la respuesta, más rápida será y más difícil será de invalidar, lo cual es la tensión central de todo este tema.
Cache hit rate: la métrica decisiva
La salud de una caché se mide a través de su hit rate: los aciertos (hits) divididos entre la suma de aciertos y fallos (misses). Es el único número que te indica si la caché está siendo útil.
hit rate = hits / (hits + misses)
La aritmética es drástica. Con un hit rate del 90%, la base de datos recibe una de cada diez solicitudes, lo que supone una reducción de 10x. Al 50%, recibe una de cada dos, solo una reducción de 2x, y la caché ha añadido un salto de red a la mitad del tráfico. Una caché con un hit rate bajo puede resultar más lenta que no tener ninguna caché.
Mídelo directamente desde el almacenamiento. Redis reporta keyspace_hits y keyspace_misses en INFO stats, y las librerías de cliente exponen los mismos contadores. Vigila el hit rate después de cada cambio en una clave, un TTL o una consulta; un pequeño cambio en una clave puede desplomar silenciosamente una caché del 95% al 40%.
El corolario es que debes cachear elementos que realmente se repitan. Una clave única por solicitud tiene, por definición, un hit rate del 0% y solo sirve para desperdiciar memoria.
Los cuatro patrones principales
Casi cualquier implementación de caché sigue una de estas cuatro estructuras. Se diferencian en quién llena la caché y cuándo se actualiza.
Cache-aside (carga perezosa)
La aplicación es la dueña de la lógica. En una lectura, verifica la caché y, si hay un fallo (miss), carga los datos desde la fuente y escribe el resultado de vuelta. Este es el patrón por defecto porque es sencillo, funciona con cualquier almacén y solo almacena en caché los datos que realmente se solicitan.
async function getUser(id: string) {
const key = `user:${id}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const user = await db.user.findUniqueOrThrow({ where: { id } });
await redis.set(key, JSON.stringify(user), "EX", 600);
return user;
}
El coste es que el primer lector siempre paga el precio completo, y la caché puede contener datos obsoletos hasta que expire su TTL o una escritura los elimine.
Read-through
Read-through desplaza la alternativa de respaldo (fallback) a la propia capa de caché: la aplicación siempre consulta la caché, y esta está configurada con un cargador que se ejecuta en caso de fallo. Algunas librerías y proxies implementan esto, por lo que el código de la aplicación no tiene una ruta de fallo explícita. El comportamiento es el mismo que en cache-aside; solo cambia la ubicación de la lógica.
Write-through
Write-through actualiza la caché y la fuente en la misma operación. Las lecturas siempre están “calientes” y nunca ven datos más antiguos que la última escritura exitosa.
async function updateUser(id: string, patch: Partial<User>) {
const user = await db.user.update({ where: { id }, data: patch });
await redis.set(`user:${id}`, JSON.stringify(user), "EX", 600);
return user;
}
El coste es la latencia de escritura y que almacena en caché datos que podrían no leerse nunca, pero mantiene la caché consistente con la ruta de escritura, que es exactamente lo que necesitan las actualizaciones orientadas al usuario.
Write-behind (write-back)
Write-behind escribe en la caché inmediatamente y vuelca los datos a la fuente de forma asíncrona. Las escrituras son muy rápidas y pueden agruparse (batching), lo cual es valioso para contadores y telemetría. El riesgo es grave: si la caché muere antes del volcado, la escritura se pierde. Úsalo solo donde sea aceptable perder las últimas escrituras, y nunca para transacciones monetarias o registros oficiales.
TTLs y frescura
Cada valor almacenado en caché debe tener un time to live (TTL). El TTL es un presupuesto de obsolescencia: es el tiempo máximo que un lector puede ver un valor que ya no es válido en la fuente. Elegirlo es tanto una decisión de producto como técnica.
- Precios e inventario: segundos. Un precio incorrecto se traduce en un ticket de soporte.
- Perfiles, feeds y listados: minutos. Una pequeña desviación es invisible.
- Datos de referencia, feature flags y configuración: de minutos a horas.
- Assets estáticos y contenido que cambia raramente: días, versionados por nombre de archivo.
Añade jitter al TTL. Si mil claves se escriben en el mismo momento con el mismo TTL, expirarán juntas y producirán un pico de carga. Aleatorizar un pequeño porcentaje distribuye las recargas.
const ttl = 600 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(value), "EX", ttl);
Incluso con una invalidación explícita, mantén un TTL como red de seguridad. Un bug que olvide eliminar una clave debería costarte unos minutos de obsolescencia, no una mentira permanente.
Invalidación: la parte difícil
Hay una razón por la cual la invalidación de caché es el remate del chiste más antiguo de la informática. Almacenar el valor es trivial, pero eliminarlo en el momento justo, de cada capa y sin condiciones de carrera, es genuinamente difícil.
Tres técnicas cubren casi todos los casos.
Versionado de claves. En lugar de eliminar, cambia la clave. Añade un prefijo de versión a las claves que incrementes cuando los datos subyacentes cambien; así, las entradas antiguas se vuelven inaccesibles y expiran por sí solas.
const version = (await redis.get(`user:${id}:v`)) ?? "1";
const key = `user:${id}:v${version}`;
Este método está libre de condiciones de carrera y funciona entre instancias, razón por la cual es el enfoque preferido para cualquier recurso compartido.
Invalidación explícita (bust). Elimina la clave al escribir. Es el enfoque más obvio y el más fácil de implementar mal, ya que cada ruta de escritura debe recordar hacerlo.
await db.user.update({ where: { id }, data: patch });
await redis.del(`user:${id}`);
Invalidación basada en eventos. Publica un evento de cambio y deja que cada instancia y capa reaccione. Así es como se mantiene la coherencia en una purga de CDN o en una caché de múltiples servicios, y es escalable a sistemas que no controlas.
Cualquiera que sea tu elección, sé consistente con el orden y prefiere eliminar antes que sobrescribir durante una actualización. Eliminar es idempotente; sobrescribir puede resucitar un valor que un escritor concurrente ya había superado.
Cache stampede y thundering herd
Cuando una clave popular expira, todas las solicitudes en curso fallan en el mismo instante. Si llegan mil solicitudes por segundo y la clave tarda 200 ms en reconstruirse, cientos de consultas idénticas golpean la base de datos simultáneamente. Esto es un cache stampede, también llamado thundering herd, y puede tumbar un servicio que estaba saludable un momento antes.
Cuatro mitigaciones, en orden aproximado de preferencia:
- Stale-while-revalidate. Sirve el valor obsoleto inmediatamente y actualízalo en segundo plano. Los lectores nunca esperan y la fuente recibe una sola actualización. HTTP tiene un encabezado exactamente para esto.
- Locking o single-flight. Solo el primer llamador vuelve a computar; todos los demás esperan brevemente o reciben datos obsoletos. Redis
SET key value NX EXes un bloqueo distribuido sencillo. - TTLs con jitter. Distribuye las expiraciones para que todas las claves no mueran al mismo tiempo.
- Expiración temprana probabilística. Cada lectura tiene una probabilidad pequeña y creciente de actualizarse antes de que la clave expire realmente, de modo que la actualización se distribuya entre muchas solicitudes.
El bloqueo siempre debe tener una expiración, o de lo contrario, un worker que falle dejará la clave bloqueada para siempre.
Qué cachear y qué no hacer jamás
Cachea los datos que sean costosos de producir, que se lean con mucha más frecuencia de la que cambian, que toleren un ligero desfase (staleness) y que sean compartidos por muchos lectores. Los listados de productos, perfiles públicos, configuraciones, fragmentos renderizados y agregados costosos son buenos candidatos.
Nunca cachees:
- Decisiones de autorización en una caché compartida. Un cambio de rol o el cierre de sesión deben surtir efecto inmediatamente, y un CDN jamás debe servir la respuesta privada de un usuario a otro.
- Datos sensibles por usuario bajo una clave compartida. Cada valor específico de usuario necesita una clave que incluya al usuario, y una directiva de caché
privatesi llega a alcanzar el edge. - Secretos y credenciales. Una caché es otro lugar desde donde pueden filtrarse.
- Datos que no puedas invalidar. Si un valor no tiene una clave natural ni un evento asociado, la caché eventualmente servirá algo incorrecto sin forma de solucionarlo.
Una prueba útil: si no puedes describir cómo sale un valor de la caché, no lo pongas ahí.
Almacenamiento en caché de HTTP en el edge
La caché más económica es aquella que nunca llega a tu servidor. HTTP te ofrece un control preciso sobre ella.
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Vary: Accept-Encoding
max-age define cuánto tiempo puede el navegador reutilizar la respuesta. s-maxage lo anula para cachés compartidas, como un CDN. stale-while-revalidate permite que el edge sirva una copia obsoleta mientras obtiene una actualizada en segundo plano, lo que evita las avalanchas de peticiones (stampedes) para contenido público. private y no-store mantienen las respuestas sensibles totalmente fuera de las cachés compartidas.
Un ETag permite realizar peticiones condicionales. El cliente envía If-None-Match y, si la etiqueta aún coincide, respondes con un 304 Not Modified sin cuerpo, ahorrando ancho de banda y garantizando que el contenido esté actualizado.
app.get("/posts", async (req, res) => {
const body = JSON.stringify(await listPosts());
const tag = `"${createHash("sha256").update(body).digest("hex")}"`;
res.set("Cache-Control", "public, max-age=30, stale-while-revalidate=60");
res.set("ETag", tag);
if (req.headers["if-none-match"] === tag) return res.status(304).end();
res.type("application/json").send(body);
});
Vary es fácil de olvidar y es importante: si una respuesta depende de Accept-Encoding o Accept-Language, indícalo; de lo contrario, una caché podría entregar la variante incorrecta al cliente equivocado.
In-memory vs Redis vs CDN
Las tres capas compartidas no son competidoras; son una jerarquía.
Un caché in-process es la búsqueda más rápida posible porque nunca atraviesa la red. Es ideal para datos pequeños, muy solicitados y mayormente de lectura, como configuraciones o un mapa de permisos. Sus limitaciones son que cada instancia tiene su propia copia (por lo que la memoria se multiplica y los valores pueden divergir) y que desaparece al hacer un deploy.
Redis es el caché compartido de la aplicación. Una única copia lógica sirve a todas las instancias, sobrevive a los reinicios y soporta TTLs, operaciones atómicas y estructuras de datos más allá de simples strings. Su coste es un viaje de ida y vuelta por la red, que suele ser inferior a un milisegundo dentro de la misma red.
Un CDN es la capa más externa. Almacena en caché respuestas públicas cerca de los usuarios en todo el mundo y puede absorber un tráfico enorme antes de que llegue a ti. Solo funciona para respuestas que son idénticas para muchos usuarios, y purgarlo es una operación deliberada y, a veces, lenta.
Una estructura común en producción es un pequeño caché in-process delante de Redis para las claves más solicitadas, con un CDN delante de toda la API para las peticiones GET públicas.
Caching negativo
Normalmente se describe que las cachés almacenan valores, pero almacenar la ausencia de un valor es igual de útil. Si una consulta para un registro inexistente es costosa y se repite, almacena el fallo (miss).
const hit = await redis.get(key);
if (hit === "__miss__") return null;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
await redis.set(key, "__miss__", "EX", 60);
return null;
}
Mantén los TTL negativos cortos, ya que un registro que no existía hace un minuto podría existir ahora. Este patrón protege contra la cache penetration, que ocurre cuando una avalancha de solicitudes de claves inexistentes evade la caché y golpea la base de datos en cada ocasión. Protégete de un atacante que genere infinitas claves inexistentes únicas validando la entrada y aplicando rate limiting antes de llegar a la caché.
Consistencia y los dos problemas difíciles
Una caché hace que un sistema sea eventualmente consistente: durante un breve periodo, diferentes lectores pueden ver valores distintos. Por lo general, esto no es un problema, pero hay situaciones en las que sí lo es.
- Read-your-writes. Un usuario que acaba de actualizar su perfil espera ver el cambio. Invalida la caché al escribir, o lee directamente de la fuente durante un corto periodo después de que el mismo usuario haya realizado una escritura.
- Replication lag. Si las lecturas se dirigen a una réplica, una caché llenada desde dicha réplica puede tener un retraso respecto a la primaria. Invalida la caché desde la ruta de escritura de la primaria.
- Cachés entre regiones. Una purga en una región no llega instantáneamente a otra. Utiliza claves versionadas o acepta el retraso de propagación.
El planteamiento honesto es que el almacenamiento en caché intercambia consistencia por velocidad, y la única forma de hacerlo de manera segura es decidir explícitamente qué nivel de obsolescencia es aceptable e integrar la invalidación en la ruta de escritura en lugar de añadirla como un parche posterior.
Las claves de caché son una API
Una clave de caché parece un detalle de implementación, pero se comporta como una interfaz pública. Es el punto de acuerdo entre cada lector y escritor, y es visible en Redis, en los slow logs y en los dashboards. Diseña las claves deliberadamente.
Tres reglas para mantener la coherencia:
- Usa namespaces por versión y entorno.
v1:user:42te permite cambiar la estructura más adelante y evita que staging comparta claves con producción. - Sé determinista. La misma búsqueda lógica siempre debe producir la misma cadena de texto. Normaliza las mayúsculas y minúsculas, recorta los espacios de entrada y nunca incluyas un valor que cambie en cada llamada.
- Nunca pongas secretos ni datos personales en una clave. Las claves se registran en logs, se exportan y son visibles para los operadores.
export const cacheKeys = {
user: (id: string) => `v1:user:${id}`,
userPosts: (id: string, cursor: string) => `v1:user:${id}:posts:${cursor}`,
productBySku: (sku: string) => `v1:product:sku:${sku.trim().toLowerCase()}`,
};
Centralizar la construcción de claves en un solo módulo es lo que hace posible la invalidación. Cuando una escritura necesita eliminar una clave, llama a la misma función que utilizó la lectura; cuando la estructura cambia, hay un único lugar para actualizar la versión.
Políticas de desalojo (Eviction policies)
Una caché a la que se le permite crecer sin límites es, básicamente, una fuga de memoria con pasos adicionales. Tanto Redis como las cachés en proceso necesitan un límite y una política sobre qué eliminar cuando se alcanza dicho límite.
Redis ofrece maxmemory y maxmemory-policy. La opción habitual para una caché pura es allkeys-lru, que desaloja la clave menos utilizada recientemente, o allkeys-lfu, que favorece las claves utilizadas frecuentemente cuando los patrones de acceso están sesgados.
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
Nunca utilices noeviction para una caché: una vez que la memoria esté llena, las escrituras fallarán y tu aplicación empezará a lanzar errores en lugar de simplemente fallar en la búsqueda (cache miss). Para las cachés en proceso, utiliza una librería LRU acotada en lugar de un Map simple, y define tanto un número máximo de entradas como un tamaño máximo.
Los desalojos no son fallos, pero un aumento en el recuento de desalojos es una señal. Significa que el conjunto de trabajo ya no cabe y que la tasa de aciertos (hit rate) está a punto de caer. La solución es asignar más memoria a la caché o cachear menos elementos.
Cache warming
Una caché está más fría en el momento más peligroso: justo después de un deploy, un reinicio o un escalado. Cada instancia comienza vacía, el tráfico llega a pleno volumen y la fuente de datos recibe toda la carga hasta que la caché se llena. Esto es un stampede provocado por tu propio despliegue.
Existen dos enfoques. El lazy warming acepta la ventana de caché fría y confía en el bloqueo (locking) y en el stale-while-revalidate para sobrevivir. Es sencillo y no requiere maquinaria adicional. El proactive warming ejecuta una tarea que carga las claves conocidas como “hot keys” antes de que la nueva versión reciba tráfico.
async function warmCache() {
const popular = await db.product.findMany({
orderBy: { views: "desc" },
take: 500,
select: { id: true },
});
for (const { id } of popular) {
await cached(`product:${id}`, 300, () =>
db.product.findUniqueOrThrow({ where: { id } }),
);
}
}
Calienta solo aquello que sabes que es demandado. Calentar todo es simplemente una copia lenta y costosa de la base de datos que, en su mayoría, será expulsada sin haber sido utilizada.
Observabilidad para cachés
Una caché que no mides es una caché en la que no puedes confiar. Hay cuatro métricas que cuentan la historia completa:
- Hit rate — ¿se están sirviendo realmente las lecturas desde la caché?
- Latencia — ¿es un hit significativamente más económico que la fuente original, incluyendo el salto de red?
- Evictions — ¿el conjunto de datos activos está superando la capacidad de la memoria?
- Errores y timeouts — ¿se está convirtiendo la caché en una fuente de fallos?
redis-cli INFO stats | grep -E 'keyspace_(hits|misses)|evicted_keys'
redis-cli INFO memory | grep used_memory_human
Emite también un contador de hits y misses desde la aplicación, etiquetado por el nombre de la caché. Esto es lo que te permite correlacionar una caída en el hit-rate con un deploy, y es lo primero que debes revisar cuando la carga de la base de datos aumenta sin una razón obvia.
Degradación progresiva cuando el cache no está disponible
Si la pérdida del cache provoca la caída de la API, el cache se ha convertido en un punto único de fallo para un sistema cuyo propósito fundamental es ser opcional. La fuente de verdad sigue existiendo; la aplicación debería ser capaz de acceder a ella.
Envuelve cada operación de cache para que un fallo se convierta en un miss en lugar de un error, mantén los timeouts cortos y considera implementar un circuit breaker que deje de llamar a un cache que esté fallando durante un periodo de enfriamiento.
async function safeGet(key: string) {
try {
return await redis.get(key);
} catch (err) {
logger.warn({ err, key }, "cache_read_failed");
return null;
}
}
Lo mismo se aplica a las escrituras en el cache: un set fallido nunca debería hacer que la solicitud falle. La única excepción es el write-behind, donde el cache es parte de la ruta de escritura; esa es otra razón para reservarlo únicamente para datos que te puedas permitir perder.
Cache de consultas costosas y agregados
No todo lo que vale la pena cachear es una sola fila. Los joins costosos, los agregados de dashboards y los resultados de búsqueda suelen ser los mejores candidatos, ya que son lentos de computar y cambian con poca frecuencia.
Una vista materializada es un cache que reside en la base de datos: almacena el resultado de una consulta y se actualiza según un horario o bajo demanda. Te ofrece la frescura de un batch job y la velocidad de lectura de una tabla.
CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at) AS day,
sum(total_cents) AS revenue_cents
FROM orders
WHERE status = 'paid'
GROUP BY 1;
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;
Para resultados que son demasiado dinámicos para una vista, cachea la respuesta serializada en Redis bajo una clave que incluya cada entrada que la afecte: filtros, orden de clasificación y página. Dos solicitudes para páginas diferentes representan valores distintos, por lo que deben tener claves diferentes.
Probando una caché
Los errores de caché son de esos que pasan las pruebas pero fallan en producción, así que prueba explícitamente los comportamientos que importan: la ruta de fallo (miss), la ruta de acierto (hit), la invalidación y el error.
test("loads once and serves the second read from cache", async () => {
let calls = 0;
const load = async () => {
calls += 1;
return { id: "1" };
};
await cached("user:1", 60, load);
await cached("user:1", 60, load);
expect(calls).toBe(1);
});
test("falls back to the source when the cache is unavailable", async () => {
redis.get = async () => {
throw new Error("connection_refused");
};
await expect(cached("user:1", 60, load)).resolves.toEqual({ id: "1" });
});
Usa un Redis real en un contenedor para las pruebas de integración, o un fake en memoria que implemente get, set y del. Prueba también los casos negativos: una actualización que debería invalidar, un TTL que debería expirar y una caída de la caché que debería degradar el servicio en lugar de fallar por completo.
Mejores prácticas
- Implementa el caché solo después de medir; añade un caché a una lectura lenta y repetitiva, no por defecto.
- Asigna un TTL a cada clave, incluso cuando realices invalidaciones explícitas.
- Utiliza claves deterministas y con namespaces, como
user:42:profile, y asígnales una versión para los datos compartidos. - Prefiere eliminar o versionar las claves en lugar de sobrescribirlas al actualizar.
- Añade jitter a los TTL y utiliza stale-while-revalidate para sobrevivir a los stampedes.
- Mantén los datos de autorización y los datos por usuario fuera de los cachés compartidos; marca las respuestas como
private. - Trata el caché como algo opcional en el código para que una caída del servicio degrade la experiencia en lugar de provocar un fallo total.
- Monitoriza la tasa de aciertos (hit rate) y el recuento de evicciones, no solo la latencia.
- Almacena en caché tanto los aciertos como los fallos (misses) cuando las búsquedas de datos ausentes sean costosas.
Errores comunes
- Implementar caching sin un TTL y depender de una purga manual que nunca ocurre.
- Compartir una misma clave entre usuarios y filtrar los datos de una persona a otra.
- Invalidar el caché en algunas rutas de escritura pero no en otras.
- Establecer el mismo TTL para todas las claves, provocando que todas expiren al mismo tiempo.
- Cachear una decisión de autorización que debería haber sido revocada tras un cambio de rol.
- Usar el caché como base de datos y perder escrituras cuando se reinicia.
- Olvidar
Varyy servir una codificación de contenido incorrecta desde un CDN. - No medir nada, haciendo que un cambio en una clave reduzca la tasa de aciertos (hit rate) sin que nadie lo note.
- Cachear algo que es único por solicitud, lo que garantiza una tasa de aciertos del 0%.
Próximos pasos
El almacenamiento en caché es una herramienta para controlar la carga; sus complementos naturales son aquellos que limitan el volumen de trabajo. Redis profundiza en el almacén sobre el cual se construyen la mayoría de las cachés, y la Paginación es la versión a nivel de solicitud de la misma idea: nunca hagas más trabajo del estrictamente necesario. Si la base de datos detrás de la caché es el cuello de botella, el Connection Pooling reduce el coste de cada conexión, y PostgreSQL cubre la fuente de verdad que estás protegiendo.