Cómo funciona el almacenamiento en caché en Node.js
El almacenamiento en caché es una apuesta: que recalcular o volver a obtener algún dato es lo suficientemente costoso, y que esos datos cambian lo suficientemente lento, como para que valga la pena almacenar una copia en un lugar más rápido y arriesgarse a que esa copia quede obsoleta.
Cada decisión de almacenamiento en caché en un servicio Node —qué almacenar en caché, por cuánto tiempo, en proceso o en Redis, cache-aside o write-through— es en realidad una decisión sobre cuánto riesgo de obsolescencia es aceptable a cambio de cuánta velocidad y costo se ahorra.
Conceptos básicos de almacenamiento en caché cubre los patrones concretos (cache-aside, estrategia TTL, prevención de estampidas); esta página trata sobre el compromiso subyacente a todos ellos, y específicamente lo que Redis añade que una caché en proceso no puede.
Una caché almacena una copia más barata de leer de datos que son costosos de calcular o obtener, aceptando un riesgo acotado de que la copia esté obsoleta a cambio de una menor latencia y una carga reducida en el origen.
Por qué es importante: Cada caché es una segunda fuente de verdad que puede discrepar silenciosamente con la real; entender cuándo y por qué puede estar equivocada es lo que diferencia una caché que ayuda de una que sirve datos incorrectos a los usuarios.
Conceptos clave:origen, cache-aside, TTL (tiempo de vida), invalidación, coherencia de caché, estampida.
Cuándo usarlo: Datos que se leen con mucha más frecuencia de la que cambian, cálculos o agregaciones costosas, resultados de llamadas externas lentas y cualquier origen (base de datos, API) cuya carga deba protegerse de picos de tráfico de lectura.
Limitaciones / Compromisos: Una caché añade un modo de fallo que el origen no tenía (obsolescencia, o que la propia caché se caiga); nunca acelera las escrituras y añade una superficie operativa real: desalojo, límites de memoria y un segundo sistema que debe mantenerse disponible.
Temas relacionados: agrupación de conexiones y carga de la base de datos, bloqueo distribuido, almacenamiento de sesiones, encabezados de control de caché y CDN.
Cada caché se sitúa entre un lector y un origen —la verdadera fuente de verdad, ya sea una base de datos, una API externa o un cálculo costoso en proceso— y su único propósito es responder a las lecturas sin molestar a ese origen cada vez.
El mecanismo que casi todos los servicios Node utilizan es cache-aside: el código de la aplicación comprueba primero la caché, y solo si hay un fallo, acude al origen, almacenando el resultado de nuevo en la caché antes de devolverlo; la caché nunca habla con el origen por sí misma, la aplicación siempre media.
const cached = await redis.get(key);if (cached) return JSON.parse(cached); // acierto de caché - el origen nunca se tocaconst value = await loadFromOrigin(); // fallo de caché - ir a la fuente de verdadawait redis.set(key, JSON.stringify(value), "EX", 300); // almacenar para la próxima vezreturn value;
En el momento en que un valor se copia en una caché, se convierte en un segundo registro de esos datos que existen independientemente del primero, lo que significa que ahora puede estar equivocado de una manera que el origen, al ser la fuente de verdad, estructuralmente no puede.
Una analogía simple: una caché es como una nota adhesiva con un número de teléfono copiado de tu libreta de direcciones; es rápido de consultar, pero si el número real cambia y nadie actualiza la nota adhesiva, marcarás con confianza un número que ya no funciona.
TTL (tiempo de vida) es el instrumento contundente que toda caché utiliza para limitar ese riesgo sin necesidad de saber exactamente cuándo cambió un valor: en lugar de intentar detectar cada escritura en el origen, un valor en caché simplemente expira después de una ventana fija, garantizando que la obsolescencia nunca exceda esa ventana, incluso si nada más le dice a la caché que se actualice.
La invalidación —eliminar o actualizar activamente un valor en caché en el momento en que su origen cambia, en lugar de esperar a que expire un TTL— es la parte difícil del almacenamiento en caché, y es difícil por una razón estructural: el código que escribe en el origen y el código que lee de la caché a menudo están en lugares diferentes, a veces en servicios completamente diferentes, por lo que nada conecta automáticamente "los datos cambiaron" con "la copia en caché ahora está mal".
La famosa frase al respecto ("solo hay dos cosas difíciles en la informática: la invalidación de caché y nombrar cosas") es una broma sobre un problema real: una caché sin invalidación solo es tan fresca como lo permite su TTL, y una caché con invalidación imperfecta puede servir datos obsoletos indefinidamente si alguna ruta de escritura olvida borrar la clave correcta.
El papel específico de Redis en esta imagen es que es una caché compartida y fuera de proceso en lugar de una en proceso, y esa distinción cambia completamente el tipo de problema de corrección con el que estás lidiando.
Sin Redis (caché en proceso): Con Redis (caché compartida):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│instancia A│ │instancia B│ │instancia A│ │instancia B│
│ caché: {}│ │ caché: {}│ └────┬────┘ └────┬────┘
└─────────┘ └─────────┘ └──────┬──────┘
posibles valores diferentes │
para la misma clave entre instancias ┌────────┐
│ Redis │ un valor compartido
└────────┘ por clave
Una caché en proceso (en memoria) es rápida —sin salto de red— pero cada instancia de servidor tiene su propia copia independiente, por lo que dos instancias pueden discrepar sobre el valor de la misma clave en el mismo momento, y una escritura en una instancia no puede invalidar la copia que reside en la memoria de otra instancia.
Redis mueve la caché completamente fuera del proceso: cada instancia lee y escribe en el mismo almacén compartido a través de la red, por lo que hay exactamente un valor en caché por clave en toda la flota, a costa de un viaje de ida y vuelta por la red por cada acceso a la caché, y una nueva dependencia que a su vez debe mantenerse disponible.
Ese costo de red también es la razón por la que la serialización importa más con Redis que con una caché en proceso: los valores deben convertirse en una cadena o carga útil de bytes (generalmente JSON) para cruzar la red y regresar, lo que representa un costo real y medible de CPU y ancho de banda que escala con el tamaño y la frecuencia de acceso de un objeto en caché.
La estampida de caché es lo que ocurre cuando una clave popular expira y muchas solicitudes concurrentes fallan a la vez, todas corriendo simultáneamente a la misma llamada costosa al origen; el pico de tráfico exacto que la caché existía para evitar, reproducido momentáneamente en su totalidad en el instante en que la caché no puede ayudar.
Las defensas estándar son un bloqueo de un solo vuelo (una solicitud repobla la caché mientras otras esperan o sirven el valor anterior un poco más) o TTL con fluctuación (aleatorizando ligeramente la expiración por clave para que muchas claves establecidas en el mismo momento no expiren todas en el mismo instante); Bloqueos distribuidos cubre la mecánica de coordinación que realmente necesita un bloqueo de un solo vuelo.
La coherencia de caché —la garantía de que todos los lectores ven el mismo valor al mismo tiempo— es fundamentalmente más débil con una caché que con el origen mismo, y eso no es un error que deba corregirse, sino una propiedad sobre la que hay que razonar explícitamente: una caché con un TTL de 5 minutos está prometiendo como máximo 5 minutos de obsolescencia, no prometiendo frescura, y cada consumidor de esos datos en caché debe ser capaz de tolerar esa ventana.
Aquí es también donde el almacenamiento en caché se conecta directamente con la carga de la base de datos: una caché bien ubicada absorbe el tráfico de lectura que de otro modo golpearía un pool de conexiones con un límite de concurrencia estricto, razón por la cual el almacenamiento en caché y el dimensionamiento del pool de conexiones generalmente se ajustan juntos en lugar de de forma independiente; una caché que protege la base de datos cambia lo que significa "suficiente capacidad del pool".
Finalmente, una caché siempre debe degradarse, no encadenarse; si Redis deja de estar disponible, el comportamiento correcto es volver al origen (con una advertencia registrada), no devolver errores a los usuarios; una caché que se cae debería hacer que un servicio sea más lento, nunca que falle por completo, ya que el origen fue la verdadera fuente de verdad todo el tiempo.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
En proceso (en memoria)
Sin salto de red, más sencillo de añadir
Inconsistente entre instancias; se pierde al reiniciar
Servicios de una sola instancia, tablas de búsqueda muy activas/pequeñas
Redis (caché compartida)
Un valor consistente en todas las instancias; sobrevive a los reinicios de instancia
Viaje de ida y vuelta por la red por cada acceso; una nueva dependencia que mantener disponible
Despliegues multi-instancia, estado de sesión/límite de tasa compartido
HTTP Cache-Control / CDN
Sin implicación del servidor de origen para las respuestas en caché
Solo se ajusta a datos públicos y no personalizados
Activos estáticos, respuestas de API públicas
Caché de escritura directa (write-through)
La caché y el origen se actualizan juntos - menor riesgo de obsolescencia
Las escrituras pagan el costo de actualizar ambos sistemas
Datos donde la obsolescencia es costosa y el volumen de escritura es manejable
"El almacenamiento en caché también acelera las escrituras." El almacenamiento en caché solo acelera las lecturas que aciertan en la caché; las escrituras aún tienen que llegar al origen, y una estrategia de escritura directa añade una actualización de caché además de eso, no en su lugar.
"Un TTL más largo siempre es más seguro que uno más corto." Un TTL más largo reduce la carga del origen pero amplía la ventana de obsolescencia; el TTL "seguro" depende completamente de cuán tolerantes sean esos datos específicos a estar incorrectos durante ese tiempo, no de una regla general.
"Redis y una caché en proceso son intercambiables; Redis es solo la versión 'de producción'." Resuelven problemas diferentes: una caché en proceso no tiene costo de red pero tampoco consistencia entre instancias; Redis tiene costo de red pero ofrece a cada instancia la misma vista de una clave. Intercambiar uno por otro cambia el comportamiento real, no solo el rendimiento.
"Si la caché está mal, eso es un error de caché." Una caché obsoleta dentro de su ventana TTL es la caché funcionando como se diseñó, no un error; el error real es esperar que una caché garantice una frescura que nunca prometió.
"La invalidación de caché solo significa llamar a delete en la clave después de una escritura." Ese es el caso fácil; la parte difícil es garantizar que cada ruta de código que cambia el origen también sepa qué claves de caché dependen de esos datos, lo que se vuelve realmente difícil una vez que los datos relacionados abarcan múltiples claves o servicios.
¿Qué sacrifica realmente una caché a cambio de velocidad?
Garantías de corrección: un valor en caché puede estar obsoleto con respecto al origen durante el tiempo que su TTL lo permita, o hasta que algo lo invalide explícitamente. El intercambio es un riesgo acotado de obsolescencia a cambio de una menor latencia y una carga reducida en el origen.
¿Qué significa "cache-aside" mecánicamente?
El código de la aplicación comprueba primero la caché; si hay un fallo, obtiene los datos del propio origen y vuelve a escribir el resultado en la caché antes de devolverlo. La caché nunca habla directamente con el origen; la aplicación siempre media en ambas direcciones.
¿Por qué Redis es diferente de simplemente almacenar datos en un objeto JavaScript en memoria?
Un objeto en memoria es local a un proceso; cada instancia de servidor tiene su propia copia independiente que puede discrepar con las demás. Redis es un servicio separado y compartido que cada instancia lee y escribe a través de la red, por lo que hay un valor consistente por clave en toda la flota.
¿Por qué se considera difícil la invalidación de caché?
Porque el código que cambia el origen y el código que lee la caché a menudo están muy separados; nada conecta automáticamente "estos datos acaban de cambiar" con "borrar esta clave de caché", por lo que mantenerlos sincronizados depende de que cada ruta de escritura recuerde invalidar las claves correctas.
¿Qué es una estampida de caché y por qué ocurre justo cuando expira una clave?
Ocurre cuando una clave de caché popular expira y muchas solicitudes concurrentes fallan en el mismo instante, todas golpeando el origen simultáneamente para repoblarlo, reproduciendo exactamente el pico de carga que la caché estaba destinada a prevenir, durante una breve ventana justo en la expiración.
¿Debería un servicio seguir funcionando si Redis se cae?
Sí, el patrón correcto es volver al origen (registrando el fallo) para que el servicio se vuelva más lento, no se rompa. Una caché que se cae nunca debería poder dejar fuera de línea una ruta de lectura respaldada por el origen que de otro modo estaría sana.
¿Es un TTL más corto siempre la opción "correcta" para evitar datos obsoletos?
No, el TTL es un dial de compromiso, no una configuración de corrección con una única respuesta correcta. Los TTL más cortos reducen la obsolescencia pero aumentan la carga del origen; el valor correcto depende de cuánta obsolescencia puedan tolerar esos datos específicos.
¿El almacenamiento en caché reduce la presión del pool de conexiones de la base de datos?
Sí, cuando se coloca bien, las lecturas servidas desde la caché nunca llegan al pool, razón por la cual la colocación de la caché y el dimensionamiento del pool de conexiones a menudo se ajustan juntos en lugar de tratarse como preocupaciones no relacionadas.
¿Por qué el costo de serialización importa más con Redis que con una caché en proceso?
Los valores de Redis viajan a través de una conexión de red, por lo que deben serializarse (generalmente a JSON) y deserializarse en cada acceso; un costo real de CPU y ancho de banda que una referencia de objeto en proceso nunca paga, ya que ya está en la memoria del mismo proceso.
¿Cuál es la diferencia entre el almacenamiento en caché y el almacenamiento en caché de `Cache-Control` HTTP de un CDN?
Resuelven el mismo problema general en diferentes capas: el almacenamiento en caché de Cache-Control/CDN funciona para respuestas HTTP públicas y no personalizadas almacenadas en caché completamente fuera del servidor de origen, mientras que el almacenamiento en caché de Redis generalmente maneja datos personalizados o protegidos por autenticación que un CDN público no puede almacenar en caché de forma segura.
¿El uso de Redis garantiza que una caché nunca esté obsoleta?
No, Redis resuelve la consistencia entre instancias (un valor compartido por clave), no la obsolescencia con respecto al origen. Un valor en Redis aún puede estar obsoleto dentro de su ventana TTL exactamente de la misma manera que un valor en caché en proceso.