Almacenar Sesiones de Usuario en Memcached con Python
Usa Memcached como almacen de sesiones distribuido en aplicaciones web Python con pymemcache, gestion de TTL y manejo de failover.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripcion general
Memcached es un almacen key-value en memoria distribuido que funciona bien para almacenamiento de sesiones en aplicaciones web. Es simple, rapido y escalable horizontalmente. A diferencia de Redis, no tiene persistencia, estructuras de datos integradas mas alla de strings, ni replicacion — pero para datos de sesion efimeros que pueden regenerarse al re-iniciar sesion, esos tradeoffs son aceptables. A continuacion: usar pymemcache para almacenar sesiones, serializacion, gestion de TTL y failover.
Cuando Usar Esto
-
For alternatives, see Cache HTTP Responses with httpx and CacheControl in Python.
-
Almacenamiento de sesiones distribuido entre multiples servidores web
-
Datos de sesion efimeros que toleran perdida (usuarios re-inician sesion al vaciar cache)
-
Lecturas de sesion de alto throughput donde las features de Redis (persistencia, pub/sub) son innecesarias
-
Modelo de sesion key-value simple sin estructuras de datos complejas
Prerrequisitos
- Python 3.10+
- Servidor Memcached ejecutandose (local o remoto)
- Paquete
pymemcache
Solucion
1. Instalar pymemcache
pip install pymemcache
2. Cliente Basico de Memcached
from pymemcache.client.base import Client
client = Client(("localhost", 11211), connect_timeout=2, timeout=2)
# Establecer un valor con TTL (en segundos)
client.set("user:session:abc123", "alice@example.com", expire=3600)
# Obtener un valor
email = client.get("user:session:abc123") # Retorna bytes o None
# Eliminar
client.delete("user:session:abc123")
3. Session Store con Serializacion JSON
import json
import time
import secrets
from pymemcache.client.base import Client
from pymemcache import serde
client = Client(
("localhost", 11211),
serializer=serde.python_memcache_serializer,
deserializer=serde.python_memcache_deserializer,
connect_timeout=2,
timeout=2,
)
class SessionStore:
def __init__(self, client: Client, default_ttl: int = 3600):
self.client = client
self.default_ttl = default_ttl
def create_session(self, user_id: str, user_data: dict) -> str:
session_id = secrets.token_urlsafe(32)
session = {
"user_id": user_id,
"data": user_data,
"created_at": time.time(),
"last_access": time.time(),
}
self.client.set(f"session:{session_id}", session, expire=self.default_ttl)
return session_id
def get_session(self, session_id: str) -> dict | None:
session = self.client.get(f"session:{session_id}")
if session is None:
return None
session["last_access"] = time.time()
self.client.set(f"session:{session_id}", session, expire=self.default_ttl)
return session
def destroy_session(self, session_id: str) -> None:
self.client.delete(f"session:{session_id}")
def extend_session(self, session_id: str, ttl: int | None = None) -> None:
ttl = ttl or self.default_ttl
session = self.client.get(f"session:{session_id}")
if session:
self.client.set(f"session:{session_id}", session, expire=ttl)
4. Usar Sesiones en una Web App (Flask)
from flask import Flask, request, jsonify, make_response
import secrets
app = Flask(__name__)
sessions = SessionStore(client, default_ttl=3600)
@app.route("/login", methods=["POST"])
def login():
data = request.json
user = authenticate(data["email"], data["password"])
if not user:
return jsonify({"error": "Invalid credentials"}), 401
session_id = sessions.create_session(user["id"], {"email": user["email"], "role": user["role"]})
response = make_response(jsonify({"message": "Logged in"}))
response.set_cookie("session_id", session_id, httponly=True, secure=True, samesite="Lax")
return response
@app.route("/profile")
def profile():
session_id = request.cookies.get("session_id")
if not session_id:
return jsonify({"error": "Not authenticated"}), 401
session = sessions.get_session(session_id)
if not session:
return jsonify({"error": "Session expired"}), 401
return jsonify({"user_id": session["user_id"], "data": session["data"]})
@app.route("/logout", methods=["POST"])
def logout():
session_id = request.cookies.get("session_id")
if session_id:
sessions.destroy_session(session_id)
response = make_response(jsonify({"message": "Logged out"}))
response.delete_cookie("session_id")
return response
5. Cluster Memcached con Hashing Consistente
from pymemcache.client.hash import HashClient
servers = [
("cache-1.internal", 11211),
("cache-2.internal", 11211),
("cache-3.internal", 11211),
]
client = HashClient(
servers,
use_pooling=True,
max_pool_size=10,
connect_timeout=2,
timeout=2,
retry_attempts=3,
retry_timeout=1,
dead_timeout=30, # Marcar servidor como muerto por 30s despues de fallos
)
# HashClient distribuye claves entre servidores usando hashing consistente
client.set("session:abc", {"user": "alice"}, expire=3600)
session = client.get("session:abc")
6. Expiracion Deslizante de Sesion
Refrescar el TTL en cada acceso para que los usuarios activos no sean desconectados:
class SlidingSessionStore(SessionStore):
def get_session(self, session_id: str) -> dict | None:
key = f"session:{session_id}"
session = self.client.get(key)
if session is None:
return None
# Resetear TTL en acceso — ventana deslizante
self.client.set(key, session, expire=self.default_ttl)
session["last_access"] = time.time()
return session
Como Funciona
- Estructura de claves: Las sesiones se almacenan como
session:<session_id>. El ID de sesion es un string URL-safe criptograficamente aleatorio generado consecrets.token_urlsafe(32). - Serializacion: El modulo
serdede pymemcache maneja la conversion Python-to-bytes. JSON funciona para dicts simples; usapicklepara objetos complejos (pero nota las implicaciones de seguridad). - TTL: El parametro
expirede Memcached establece el TTL en segundos. Cuando expira, la clave se evicta automaticamente. No se necesita limpieza. - Hashing consistente:
HashClientdistribuye claves entre multiples servidores Memcached. Si un servidor cae, se marca como muerto y las claves se redistribuyen a los servidores restantes. - Failover:
retry_attemptsydead_timeoutcontrolan como el cliente maneja fallos de servidor. Un servidor muerto se reintenta despues dedead_timeoutsegundos.
Variantes
Middleware de Sesion para FastAPI
from fastapi import FastAPI, Request, HTTPException, Response
from fastapi.middleware import Middleware
app = FastAPI()
sessions = SessionStore(client, default_ttl=3600)
@app.middleware("http")
async def session_middleware(request: Request, call_next):
session_id = request.cookies.get("session_id")
request.state.session = None
if session_id:
request.state.session = sessions.get_session(session_id)
response = await call_next(request)
return response
@app.get("/profile")
async def profile(request: Request):
if not request.state.session:
raise HTTPException(status_code=401, detail="Not authenticated")
return {"user_id": request.state.session["user_id"]}
Sesiones Encriptadas
Encriptar datos de sesion antes de almacenar en Memcached:
from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher = Fernet(key)
class EncryptedSessionStore(SessionStore):
def create_session(self, user_id: str, user_data: dict) -> str:
session_id = secrets.token_urlsafe(32)
plaintext = json.dumps({"user_id": user_id, "data": user_data}).encode()
encrypted = cipher.encrypt(plaintext)
self.client.set(f"session:{session_id}", encrypted, expire=self.default_ttl)
return session_id
def get_session(self, session_id: str) -> dict | None:
encrypted = self.client.get(f"session:{session_id}")
if encrypted is None:
return None
plaintext = cipher.decrypt(encrypted)
return json.loads(plaintext)
Sesion con CAS (Compare-And-Swap)
Prevenir race conditions al actualizar sesiones concurrentemente:
def update_session_cas(client, session_id, update_fn):
key = f"session:{session_id}"
result = client.gets(key) # Obtiene valor + token CAS
if result is None:
return None
value, cas_token = result
updated = update_fn(value)
# Solo establece si el valor no ha cambiado desde gets()
success = client.cas(key, updated, cas_token, expire=3600)
if not success:
raise ConcurrentModificationError("Session was modified by another request")
return updated
Mejores Practicas
- Usar IDs de sesion criptograficamente aleatorios:
secrets.token_urlsafe(32)genera strings URL-safe de 43 caracteres con 256 bits de entropia. - Establecer
httponly,secure,samesiteen cookies: Previene robo de sesion por XSS y ataques CSRF. - Mantener sesiones pequenas: Memcached tiene un limite de item de 1MB por defecto. Almacena solo user ID y datos minimos — obtiene el resto de la base de datos.
- Usar expiracion deslizante para usuarios activos: Resetear TTL en cada acceso para que los usuarios no sean desconectados mientras estan activos.
- Monitorear tasa de cache miss: Alta tasa de miss significa que las sesiones expiran muy rapido o los servidores estan descartando claves.
- Usar
HashClienten produccion: Memcached de un solo servidor es un single point of failure. Ejecuta al menos 2-3 instancias.
Errores Comunes
- Almacenar objetos grandes en sesiones: El limite de 1MB de Memcached puede descartar sesiones grandes silenciosamente. Manten las sesiones bajo 10KB.
- No manejar retornos
None:client.get()retornaNonepara claves faltantes o errores de servidor. Siempre verificaNone. - Usar serializacion
pickleen datos no confiables: Deserializar pickles no confiables es un riesgo de ejecucion remota de codigo. Usa JSON. - Sin manejo de failover: Un solo servidor Memcached cayendo desconecta a todos los usuarios. Usa
HashClientcon multiples servidores. - TTL fijo sin ventana deslizante: Los usuarios son desconectados a mitad de sesion incluso si estan activos. Resetear TTL en acceso.
FAQ
Memcached vs Redis para sesiones — cual es mejor?
Redis ofrece persistencia, replicacion y estructuras de datos mas ricas. Memcached es mas simple y ligeramente mas rapido para workloads puros key-value. Si necesitas que los datos de sesion sobrevivan reinicios, usa Redis. Si las sesiones son puramente efimeras, Memcached esta bien.
Que pasa cuando Memcached se queda sin memoria?
Memcached usa eviction LRU. Cuando la memoria esta llena, los items menos recientemente usados se evictan. Esto significa que las sesiones viejas se descartan — los usuarios con sesiones viejas tendran que re-iniciar sesion.
Puedo usar Memcached para datos de sesion sensibles?
Memcached no encripta datos en reposo. Si necesitas almacenar datos sensibles, encriptalos antes de almacenar (ver la variante de Sesiones Encriptadas). La seguridad a nivel red (TLS, red privada) tambien es importante.
Como manejo fallos de servidor Memcached?
HashClient con dead_timeout marca servidores fallidos como muertos y redistribuye claves. Las sesiones en el servidor muerto se pierden — los usuarios tendran que re-iniciar sesion. Para zero-downtime, usa Redis con replicacion en su lugar.
Cual es el tamano maximo de sesion en Memcached?
El limite por defecto es 1MB por item. Puedes aumentarlo con el flag -I 5m al iniciar Memcached, pero las sesiones grandes afectan el rendimiento. Manten las sesiones pequenas.
Deberia usar Memcached o Redis para sesiones?
Memcached es mas simple y rapido para almacenamiento de sesiones key-value puro. Redis ofrece persistencia, replicacion y estructuras de datos mas ricas (lists, sets, sorted sets). Si las sesiones deben sobrevivir restarts o necesitas alta disponibilidad, usa Redis. Si quieres maxima velocidad y toleras perdida de sesiones en restart, Memcached esta bien.
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Cache Function Results with Redis and TTL in Python
Build a Python decorator that caches function return values in Redis with configurable TTL, key generation, and cache invalidation
RecipeSecure JWT Refresh Token Rotation with Python
Implement secure JWT access and refresh token rotation in Python with blacklist, reuse detection, and automatic access token renewal for stateless auth
GuideComplete Guide to API Versioning Strategies
Version REST and GraphQL APIs with URI, header, query param, and content negotiation strategies. Covers deprecation, sunset, and migration patterns.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
RecipeSecure API Gateway with Custom Lambda Authorizers
Implement custom Lambda authorizers for API Gateway with JWT validation, IAM policy generation, and caching for token-based authentication in serverless APIs.
RecipeCache Database Queries with Django Cache Framework
Use Django's built-in cache framework with per-view caching, template fragment caching, and low-level cache API for database query optimization.