Sticky sessions vs. un caché de sesión compartido: ¿cómo manejar el estado de sesión del usuario detrás de un balanceador de carga?
Fijar a un usuario a un solo servidor te hereda sus problemas de disponibilidad; mover las sesiones a un caché compartido cambia algo de latencia por escalabilidad horizontal real.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Sticky sessions vs. un caché de sesión compartido: ¿cómo manejar el estado de sesión del usuario detrás de un balanceador de carga?
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.#El problema sólo aparece con más de un servidor
Un único proceso de backend puede guardar el estado de sesión en memoria — un simple mapa hash indexado por ID de sesión — y funciona perfectamente, porque toda petición de un cliente dado llega, por definición, a ese mismo proceso. En el momento en que ponés un balanceador de carga delante de más de una instancia, ese supuesto se rompe: dos peticiones consecutivas del mismo navegador pueden aterrizar en dos servidores distintos, y el segundo servidor nunca oyó hablar de la sesión que creó el primero. El estado de login, el contenido del carrito de compras, el progreso de un formulario de varios pasos — cualquier cosa guardada en la memoria local de un servidor — parece desaparecer al azar.
Hay dos soluciones estándar, y sus costos van en direcciones casi opuestas: fijar al cliente a un solo servidor para que la memoria local siga funcionando (sticky sessions), o dejar de depender de la memoria de cualquier servidor en particular y poner el estado de sesión en un lugar al que todos puedan acceder (un caché de sesión compartido).
#Opción 1: Sticky sessions (afinidad de sesión)
El balanceador de carga se encarga de que toda petición de un cliente dado llegue siempre al mismo servidor mientras dure la sesión. Habitualmente lo hace de dos formas: insertando su propia cookie de afinidad (por ejemplo, la cookie AWSALB de un ALB de AWS) que codifica a qué instancia fue enrutado el cliente, o hasheando algo estable del cliente — la IP de origen, o una cookie propia de la aplicación — hacia un backend consistente. En cualquiera de los dos casos, el código de la aplicación no tiene que cambiar en absoluto: req.session.userId = 123 simplemente funciona, porque la sesión vive en la memoria del único proceso que sigue respondiéndole a ese cliente.
El costo aparece después, no de inmediato. El balanceo de carga se degrada de "repartir el trabajo parejo" a "repartir sesiones nuevas parejo, y después convivir con la mezcla que le tocó a cada servidor" — un servidor que por azar recibió varias sesiones largas y muy activas termina más cargado que sus vecinos, sin ninguna forma de rebalancear a mitad de sesión. Peor aún, la afinidad de sesión choca activamente con dos cosas que uno quiere en producción: los rolling deploys (drenar un servidor que está por ser reemplazado implica esperar a que sus sesiones terminen naturalmente, o cortarlas a la fuerza) y el autoscaling hacia abajo (quitar una instancia destruye todas las sesiones fijadas a ella, sin ninguna forma limpia de migrarlas antes).
#Opción 2: Un almacén de sesión compartido
Se saca el estado de sesión por completo de la memoria del proceso y se lo mueve a algo que todas las instancias puedan leer y escribir — Redis y Memcached son las opciones habituales, aunque una tabla relacional también funciona a menor escala. El balanceador de carga vuelve a poder usar round-robin puro o least-connections, porque ya no importa qué instancia atiende qué petición; cada instancia trae la misma sesión por ID desde el almacén compartido en cada petición que la necesita.
Esto cuesta un viaje de red por cada lectura o escritura de sesión — típicamente bastante por debajo del milisegundo hacia una instancia de Redis bien ubicada, algo insignificante frente al resto del presupuesto de latencia de una petición. A cambio, las sesiones sobreviven limpiamente a reinicios de instancias, deploys y eventos de scale-down, y el balanceador puede distribuir carga puramente según capacidad, sin importar dónde vivan las sesiones viejas. Eso sí, el almacén compartido se convierte en una dependencia nueva: si cae, todos los servidores pierden acceso al estado de sesión al mismo tiempo, razón por la cual en producción se lo corre con replicación y failover (Redis Sentinel o un clúster gestionado) en lugar de una sola instancia.
#Cómo elegir entre las dos
| Sticky sessions | Almacén de sesión compartido | |
|---|---|---|
| Cambios en el código de la app | Ninguno | Leer/escribir la sesión mediante una biblioteca cliente |
| Distribución de carga | Despareja con el tiempo | Pareja, independiente de la antigüedad de la sesión |
| Failover | La sesión se pierde si muere el servidor fijado | La sesión sobrevive a la caída de cualquier servidor |
| Rolling deploys | Hay que drenar o descartar sesiones por servidor | Cualquier servidor puede atender cualquier petición de inmediato |
| Dependencia nueva | Ninguna | El propio almacén compartido (necesita su propia alta disponibilidad) |
| Latencia extra por petición | Ninguna | Un viaje de red rápido |
Vale nombrar un tercer camino, aunque en rigor no sea "almacenamiento de sesión" en absoluto: los tokens sin estado (JWT) empujan los datos de la sesión hacia un token firmado que el cliente guarda y reenvía en cada petición, de modo que no hace falta ninguna consulta del lado del servidor. Eso cambia un problema de almacenamiento por uno de revocación — no podés invalidar barato un JWT individual antes de que venza su claim exp, sólo rodear el problema (vencimientos cortos más un refresh token, o una lista negra que reintroduce exactamente el mismo problema de estado compartido que estabas tratando de evitar).
🧠 Check your understanding
0/2 · 0/2 answered1. ¿Cuál es el principal riesgo operativo que introducen las sticky sessions durante un rolling deploy?
2. ¿Cuál es el costo principal de latencia al mover el estado de sesión de la memoria local a un almacén Redis compartido?