Los sistemas en producción no fallan porque elegiste “la base de datos equivocada”. Fallan porque elegiste la base de datos adecuada para el comportamiento equivocado.
Sesiones que sobreviven a despliegues, límites de tasa que no deben reiniciarse al reiniciar, colas que no pueden perder un trabajo pero tampoco bloquear el sistema: esos son comportamientos.
Si almacenas todo en el mismo lugar porque es conveniente, lo pagarás después. Usualmente a las 2:17 AM, cuando tu “cache simple”
se convierte en tu sistema de autenticación y el canal de incidentes empieza a hacer cardio.
Marco de decisión: elige por modo de fallo, no por sensaciones
PostgreSQL y Redis se solapan lo suficiente como para tentarte a tomar una decisión dogmática. Resiste eso. Elige según lo que debe ser cierto
durante fallos: reinicios, particiones, sobrecarga, escrituras parciales, desajuste de reloj, reintentos de clientes y despliegues progresivos.
Una regla contundente
Si perderlo es aceptable (o reconstruible), Redis es un gran valor por defecto. Si perderlo es inaceptable (o legal/financieramente doloroso),
PostgreSQL es el adulto en la sala.
Para qué sirve cada sistema en la práctica
- Redis: memoria compartida rápida con red, operaciones atómicas, expiraciones y varias estructuras de datos. La durabilidad es opcional y matizada.
- PostgreSQL: un sistema transaccional con write-ahead logging, restricciones, capacidad de consulta y semánticas duraderas bajo condiciones bien entendidas.
Cómo suena el mismo requisito diferente en una revisión de incidentes
“Almacenamos sesiones en Redis” es una afirmación de diseño. “Un reinicio de Redis desconectó a todos” es una afirmación de incidente.
Tu trabajo es traducir afirmaciones de diseño a afirmaciones de incidente antes de que la producción lo haga por ti.
Una cita sobre fiabilidad (idea parafraseada)
Idea parafraseada: la esperanza no es una estrategia
— atribuida a muchos líderes de operaciones; el sentimiento es común en ingeniería de fiabilidad.
Dos chistes cortos (exactamente dos)
Chiste #1: Un caché es donde los datos van a jubilarse. A menos que almacenes autenticación en él; entonces se convierte en una carrera con guardias nocturnos.
Chiste #2: Cada clave “temporal” de Redis vive exactamente tanto como tu rotación de on-call.
Matriz de decisión (úsala cuando la gente discuta)
Haz estas preguntas en orden; detente cuando obtengas un “no” rotundo.
- ¿Puedes tolerar perderlo? Si no: prefiere PostgreSQL (u otro almacén/cola duradera).
- ¿Necesitas incrementos atómicos/expiraciones a alta tasa? Si sí: Redis suele ganar.
- ¿Necesitas consultas ad-hoc, auditoría o backfills? Si sí: PostgreSQL gana.
- ¿Necesitas fan-out / streams / consumer groups? Redis puede ser excelente, pero comprométete a operarlo como un sistema, no como un juguete.
- ¿Necesitas acoplamiento transaccional estricto con escrituras de negocio? Si sí: PostgreSQL, porque “escribir en la app + escribir en Redis” es donde la consistencia va a morir.
Modelo de amenazas: los fallos para los que debes diseñar
- Reinicio: reinicio de procesos, reinicio de nodos, reprogramación de contenedores.
- Partición: la app puede alcanzar un nodo del datastore pero no otro; los clientes reintentan.
- Sobrecarga: picos de latencia, acumulación de trabajo, timeouts que se convierten en reintentos, reintentos que se vuelven tormenta.
- Tiempo: TTL y ventanas de límites de tasa basadas en tiempo; los relojes se desvían, los despliegues ruedan, los usuarios son impacientes.
- Evicción: Redis puede evictar claves; “volatile-lru” no es un plan de continuidad de negocio.
- Vacuum/compaction: el bloat y vacuum de Postgres afectan la latencia; el fork de Redis para snapshots afecta memoria y latencia.
Hechos interesantes y contexto histórico (lo que cambia decisiones)
- La línea histórica de PostgreSQL se remonta a POSTGRES (década de 1980, UC Berkeley), diseñado con una obsesión investigadora por la corrección y la extensibilidad—todavía visible en MVCC y WAL.
- Redis comenzó (finales de los 2000) como un servidor de estructuras de datos en memoria; su característica clave no fue “clave/valor”, sino operaciones atómicas sobre estructuras útiles (listas, sets, sorted sets).
- La persistencia de Redis empezó como snapshots opcionales (RDB). El append-only file (AOF) llegó para reducir las ventanas de pérdida, pero intercambia durabilidad por amplificación de escritura y opciones de fsync.
- MVCC de Postgres significa que las lecturas no bloquean escrituras (en general). También significa que las tuplas muertas se acumulan; vacuum no es opcional si te gusta la latencia estable.
- La ejecución de comandos single-threaded en Redis es una característica: hace que la mayoría de operaciones sean atómicas sin locks. También es un techo cuando atas reglas Lua pesadas o comandos de larga ejecución.
- Las colas basadas en LIST en Redis fueron populares mucho antes de los streams; los patrones BRPOP moldearon una generación de sistemas “suficientemente buenos”—hasta que la gente necesitó replay y consumer groups.
- “Procesamiento exactamente una vez” ha sido un espejismo recurrente en la industria. La mayoría de sistemas reales logran al menos una vez más idempotencia. Postgres facilita imponer idempotencia con restricciones.
- La limitación de tasa evolucionó de ventanas fijas a ventanas deslizantes y token buckets porque la equidad importa cuando hay picos; los incrementos atómicos de Redis hicieron que estos patrones fueran prácticos a escala.
Sesiones: adhesivas, sin estado y la mentira intermedia
Qué significa realmente “almacenamiento de sesiones”
Las sesiones son estado, pero la aplicación quiere fingir que es sin estado. Esa tensión aparece en tres lugares:
autenticación, revocación y expiración.
La suposición peligrosa es que las sesiones son “solo caché”. No lo son, a menos que realmente no te importe que un usuario se desconecte o sea desafiado de nuevo.
Para aplicaciones de consumo puede ser aceptable. Para consolas B2B en una demo de ventas, es como conseguir tu próximo recorte presupuestario.
Tres patrones y dónde pertenecen
-
Tokens firmados (sin sesión en servidor)
No almacenar nada en el servidor; poner claims en un token firmado (tipo JWT). Genial para APIs con muchas lecturas. Terrible cuando necesitas revocación instantánea
y TTLs cortos causan tormentas de refresh. -
Sesiones en servidor
Almacena un ID de sesión en una cookie, guarda los datos de sesión en Redis o Postgres. Operativamente aburrido, lo cual es un cumplido. -
Híbrido
Token firmado para identidad + lista negra/revocación en servidor. Aquí es donde Redis es fuerte: claves pequeñas, TTLs, comprobaciones atómicas.
Redis para sesiones: cuándo es adecuado
Redis funciona bien para sesiones cuando:
- La pérdida de sesiones es aceptable (o puedes re-autenticar fácilmente).
- Necesitas lecturas muy rápidas y expiración por TTL sin un trabajo de limpieza.
- Eres disciplinado respecto a la persistencia (o eliges explícitamente ser con pérdida).
- Puedes tolerar el ocasional “todos inician sesión otra vez”.
Redis para sesiones: cuándo es una trampa
Es una trampa cuando la sesión es efectivamente un registro de derechos (roles, scopes, flags de MFA) y la pérdida significa:
que el acceso revocado vuelva, o que el acceso desaparezca para usuarios válidos. Cualquiera de los dos genera tickets de soporte.
Además: las políticas de evicción de Redis no se preocupan por tu funnel de ventas. Si la presión de memoria dispara la evicción y tus claves de sesión son elegibles,
acabas de crear una lotería de desconexiones aleatorias.
PostgreSQL para sesiones: aburrido y correcto
Las sesiones en Postgres son más lentas por petición, pero predecibles y consultables. Puedes imponer unicidad, rastrear last_seen, ejecutar auditorías
y backfills de limpieza. Y puedes acoplar cambios de estado de sesión con escrituras de negocio dentro de transacciones.
El intercambio: ahora eres responsable de la limpieza y del control del bloat. Las tablas de sesiones son fábricas de churn. Si no gestionas vacuum, verás incremento de latencia.
Diseño práctico: separa “identidad de sesión” de “payload de sesión”
Un buen compromiso: guarda el registro autoritativo mínimo en Postgres (session id, user id, created_at, revoked_at, expires_at),
y guarda payloads opcionales de rendimiento en Redis (preferencias de usuario, permisos calculados) indexados por session id con TTL.
Si Redis lo pierde, lo recomputas. Si Postgres lo pierde, tienes problemas mayores.
Límites de tasa: contadores, relojes y equidad
Qué protege la limitación de tasa
La limitación de tasa no es solo “detener abusadores”. También sirve para:
proteger dependencias downstream, moldear tenants ruidosos, evitar manadas en login o restablecimiento de contraseña,
y prevenir que reintentos autoinfligidos consuman todo tu presupuesto.
Limitación de tasa en Redis: el predeterminado por una razón
Redis es excelente para límites de tasa porque tiene:
- Incrementos atómicos (INCR/INCRBY) para no correr contra uno mismo.
- TTL (EXPIRE) para que los contadores desaparezcan sin cron jobs.
- Scripts Lua para combinar “incrementar + comprobar + fijar TTL” en una sola operación atómica.
- Baja latencia para que tu limitador no sea el cuello de botella.
Pero: la durabilidad en Redis no es gratis
Si tu limitador se reinicia, quizá estés bien. O quizá inundes a un API partner durante 90 segundos y te revoquen la clave.
Decide en qué mundo vives.
Si necesitas “sobrevivir reinicios”, configura la persistencia con cuidado y pruébala. AOF con políticas de fsync puede ayudar,
pero cambia el envelope de rendimiento y el modo de fallo (IO de disco se vuelve tu limitador).
Limitación de tasa en Postgres: cuándo debes hacerlo igualmente
Postgres puede hacer limitación de tasa, normalmente en una de estas formas:
- Contadores por ventana con upserts (INSERT … ON CONFLICT DO UPDATE). Funciona para tasas moderadas y necesidades fuertes de corrección.
- Token bucket almacenado por usuario/tenant en una fila con “last_refill” y “tokens.” Requiere manejo cuidadoso de tiempo y concurrencia.
- Leaky bucket vía jobs donde la base de datos es autoritativa y la aplicación cachea la decisión brevemente.
La limitación en Postgres es más lenta, pero te da auditoría y capacidad de consulta. Si necesitas responder “por qué el tenant X fue throttled ayer”,
Postgres es donde esa historia es más fácil de reconstruir.
La equidad es una decisión de producto disfrazada de algoritmo
Los límites de ventana fija son fáciles e injustos en los bordes. Las ventanas deslizantes son justas y caras. Los token buckets son lo suficientemente justos
y lo suficientemente baratos. El punto: elige la equidad que puedas sostener operativamente. Un algoritmo ingenioso que no puedas depurar durante un incidente
es una carga.
Colas: durabilidad, visibilidad y retropresión
Una cola no es una lista
Una cola real es un contrato:
los mensajes se registran de forma durable, los workers los reclaman, las fallas los devuelven, ocurren duplicados y el sistema sigue siendo observable.
Si implementas colas como “una lista más esperanza”, aprenderás sobre casos límite en el peor entorno posible: producción.
Colas en Redis: rápidas, flexibles y fáciles de equivocarse sutilmente
Redis puede implementar colas con listas (LPUSH/BRPOP), sets ordenados (trabajos con retraso), streams (consumer groups) y pub/sub (no es una cola).
Cada una tiene compensaciones:
- Listas: simples, rápidas. Pero necesitas tu propio modelo de fiabilidad (ack, retry, dead-letter). Los patrones BRPOPLPUSH ayudan.
- Streams: más cercanos a un log real con consumer groups, acking y entradas pendientes. Operativamente más complejo, pero más honesto.
- Pub/Sub: no duradero; los suscriptores que se desconectan pierden mensajes. Excelente para notificaciones efímeras, no para trabajos.
Colas en Postgres: sorprendentemente fuertes para muchas cargas
Postgres puede alimentar colas usando tablas + índices + bloqueo de filas:
- SELECT … FOR UPDATE SKIP LOCKED es la herramienta para “reclamar un trabajo sin que dos workers lo tomen.”
- Enqueue transaccional permite acoplar la creación de trabajos con el estado de negocio (patrón outbox).
- Capacidad de consulta te permite construir dashboards e investigar trabajos atascados sin herramientas personalizadas.
La contrapartida es rendimiento y contención. Postgres es genial hasta que no lo es: altas tasas de escritura, particiones calientes o transacciones de larga duración
pueden convertir la tabla de la cola en un campo de batalla. Pero para muchos sistemas corporativos—tasas moderadas, alta corrección—las colas en Postgres son
la opción aburrida que realmente entrega.
Timeout de visibilidad: lo que decide si duermes
Los trabajos necesitan un concepto de “en progreso”. Si un worker muere a mitad de trabajo, ese trabajo debe volverse visible otra vez. Las listas de Redis no te dan eso
automáticamente. Streams sí, pero aún necesitas manejar entradas pendientes. Postgres lo hace, si modelas “locked_at/locked_by” y reclamas trabajos
después de un timeout.
Idempotencia: procesarás duplicados
Entre reintentos, timeouts, particiones de red y despliegues, los duplicados no son hipotéticos. “Exactamente una vez” es marketing.
Construye claves de idempotencia en los handlers de trabajo y aplícalas donde sea posible (las restricciones únicas de Postgres son tus mejores aliadas).
Guion de diagnóstico rápido: encuentra el cuello de botella en 10 minutos
Te han paginado. La latencia sube. Los logins fallan. Las colas se acumulan. No debatas arquitectura. Ejecuta el guion.
Primero: determina si el problema es latencia del datastore o contención en la app
- Revisa la latencia de Redis y clientes bloqueados (si Redis está en el camino). Si ves comandos lentos o clientes bloqueados, probablemente estás atado a Redis.
- Revisa sesiones activas y locks en Postgres. Si ves esperas de lock o conexiones saturadas, probablemente estás atado a Postgres.
- Revisa patrones de error: timeouts vs “OOM” vs “too many clients” vs “READONLY” te ayudan a triagear rápidamente.
Segundo: confirma si el problema es saturación de recursos o bugs de corrección
- Saturación de recursos: CPU alta, IO alto, caídas de red, presión de memoria, evicción, pool de conexiones al máximo.
- Bugs de corrección: pico súbito de reintentos, bucle accidental, script Lua descontrolado, crecimiento desbordado de la cola por falta de ack.
Tercero: elige la mitigación menos peligrosa
- Limita más (sí, aunque producto se queje) para estabilizar.
- Desactiva características caras (enriquecimiento de sesión, chequeos profundos de permisos) si golpean el datastore.
- Pausa consumidores si la cola está derritiendo sistemas downstream.
- Escala lecturas si la carga lo permite; no añadas escritores a ciegas para arreglar un problema de locks.
Qué no hacer
No reinicies Redis como “arreglo” a menos que aceptes perder datos volátiles y sepas por qué está atascado. No reinicies Postgres cuando tengas
transacciones de larga duración a menos que disfrutes explicar a finanzas por qué se duplicaron facturas.
Tareas prácticas: comandos, salidas, qué significa, qué decides
Estas son las comprobaciones que ejecutas durante revisiones de diseño y durante incidentes. Cada una incluye un comando, salida de ejemplo,
lo que significa la salida y la decisión que impulsa.
Redis: salud, latencia, persistencia, memoria, evicción
Task 1: Check Redis basic health and role
cr0x@server:~$ redis-cli -h redis-01 INFO replication
# Replication
role:master
connected_slaves:1
master_repl_offset:987654321
repl_backlog_active:1
Significado: Estás en un master; un réplica está conectada; el backlog de replicación está activo.
Decisión: Si el rol es “slave” inesperadamente, tus clientes pueden estar escribiendo a un nodo de solo lectura. Arregla el descubrimiento de servicio/failover primero.
Task 2: Measure Redis command latency spikes
cr0x@server:~$ redis-cli -h redis-01 --latency -i 1
min: 0, max: 12, avg: 1.23 (176 samples)
min: 0, max: 97, avg: 4.87 (182 samples)
Significado: Picos ocasionales de 97ms. En una ruta de limitador de tasa, eso duele. En lecturas de sesión, puede cascada en timeouts/reintentos.
Decisión: Investiga comandos lentos, forks de persistencia o jitter de red. Si los picos se correlacionan con RDB saves, ajusta persistencia o pasa a AOF con ajustes adecuados.
Task 3: Identify slow Redis commands (built-in slowlog)
cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 3
1) 1) (integer) 12231
2) (integer) 1735600000
3) (integer) 24567
4) 1) "EVAL"
2) "..."
5) "10.0.2.41:53422"
6) ""
Significado: Un script Lua tardó ~24ms. Unos pocos de estos bajo carga pueden serializar tu servidor porque Redis ejecuta comandos mayormente en un solo hilo.
Decisión: Reescribe scripts para que sean más simples, reduce scans de claves o saca lógica pesada de Redis. Si necesitas lógica compleja de cola, considera streams de Redis o Postgres.
Task 4: Check Redis memory use and fragmentation
cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|maxmemory_human|mem_fragmentation_ratio'
used_memory_human:18.42G
maxmemory_human:20.00G
mem_fragmentation_ratio:1.78
Significado: Estás cerca de maxmemory y la fragmentación es alta. Evicciones u errores OOM están a la vuelta de la esquina; el fork para persistencia puede fallar.
Decisión: Aumenta memoria, reduce la cardinalidad de claves, arregla la estrategia de TTL o elige política de evicción intencionalmente. Si las sesiones no deben desaparecer, no confíes en políticas que favorezcan evicción.
Task 5: Confirm eviction behavior
cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"
Significado: Cualquier clave puede ser evictada bajo presión de memoria.
Decisión: Si almacenas sesiones o estado de cola aquí, esto es un riesgo de fiabilidad. Considera “volatile-ttl” con claves solo con TTL, o mueve estado autoritativo a Postgres.
Task 6: Check persistence mode and last save
cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|rdb_last_save_time|aof_last_write_status'
aof_enabled:1
aof_last_write_status:ok
rdb_last_save_time:1735600123
Significado: AOF está habilitado y escribiendo correctamente; RDB también existe. Tienes algo de durabilidad, dependiendo de la política de fsync.
Decisión: Si este Redis es ahora autoridad de sesiones o fuente de verdad de colas, verifica la política de fsync y el tiempo de recuperación. Si no puedes tolerar pérdida, Redis aún podría no ser suficiente.
Task 7: Check for blocked clients (often queue-related)
cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'blocked_clients|connected_clients'
connected_clients:1823
blocked_clients:312
Significado: Muchos clientes están bloqueados, probablemente esperando pops bloqueantes o scripts lentos. Esto puede ser normal para patrones BRPOP, pero 312 es alto.
Decisión: Si los clientes bloqueados se correlacionan con timeouts, rediseña el consumo de la cola (streams, concurrencia limitada) o mejora la eficiencia de los workers.
PostgreSQL: conexiones, locks, vacuum, bloat, IO, comportamiento de consultas
Task 8: See Postgres connection saturation
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as total, state from pg_stat_activity group by state order by total desc;"
total | state
-------+----------------
120 | active
80 | idle
35 | idle in transaction
Significado: 35 sesiones están idle in transaction. Eso suele ser un bug o una mala configuración del pool y bloquea vacuum y mantiene locks.
Decisión: Arregla la app para commit/rollback pronto; establece statement timeouts; asegura que tu pool no deje transacciones abiertas.
Task 9: Identify lock waits
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
wait_event_type | wait_event | count
-----------------+----------------+-------
Lock | transactionid | 9
LWLock | BufferMapping | 4
Significado: Esperas de lock en transaction IDs sugieren contención (updates/deletes, transacciones largas) afectando a otros.
Decisión: Encuentra la transacción que bloquea. Si es una migración o job por lotes, deténla o controla su tasa. Si son workers de colas que mantienen locks, arregla su comportamiento.
Task 10: Find the blockers
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select pid, usename, state, now()-xact_start as xact_age, query from pg_stat_activity where state <> 'idle' order by xact_age desc limit 5;"
pid | usename | state | xact_age | query
------+--------+--------+----------+------------------------------------------
8421 | app | active | 00:14:32 | update sessions set last_seen=now() ...
9110 | app | active | 00:09:11 | delete from queue_jobs where ...
Significado: Escrituras de larga duración en tablas de sessions/queue. Esa es la zona caliente para churn de sesiones y contención de colas.
Decisión: Añade índices, reduce la frecuencia de actualización (write-behind para last_seen) y asegura que los deletes de la cola se hagan en batch con límites sensatos.
Task 11: Check vacuum health on a churny table
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, n_dead_tup, last_autovacuum, autovacuum_count from pg_stat_user_tables where relname in ('sessions','queue_jobs');"
relname | n_dead_tup | last_autovacuum | autovacuum_count
-----------+------------+-------------------------+------------------
sessions | 812334 | 2025-12-30 01:10:02+00 | 421
queue_jobs| 120998 | 2025-12-30 01:08:47+00 | 388
Significado: Grandes contadores de tuplas muertas. Autovacuum está corriendo, pero puede que aún vaya atrasado bajo carga.
Decisión: Ajusta autovacuum para estas tablas (umbrales más bajos), considera particionar por tiempo y reduce el churn de actualizaciones.
Task 12: Inspect index usage for sessions or rate-limit tables
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, idx_scan, seq_scan from pg_stat_user_tables where relname='sessions';"
relname | idx_scan | seq_scan
----------+----------+----------
sessions | 98234122 | 4132
Significado: Los scans por índice dominan (bueno). Si seq_scan fuera alto, probablemente estarías haciendo scans de tabla completa en una tabla caliente.
Decisión: Si seq_scan sube, añade/arregla índices o corrige predicados de consulta. Para sesiones, quieres búsquedas por session_id y expires_at.
Task 13: Measure Postgres cache vs disk pressure (rough signal)
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) as hit_pct from pg_stat_database where datname='appdb';"
datname | blks_hit | blks_read | hit_pct
---------+-----------+-----------+---------
appdb | 891234567 | 23123456 | 97.46
Significado: El hit de cache es ~97%. Eso es aceptable. Si cae bruscamente, el IO de disco puede ser tu cuello de botella.
Decisión: Si hit_pct baja, revisa tamaño del working set, índices y si un nuevo patrón de consulta está leyendo muchos datos fríos.
Task 14: Check queue backlog and oldest job age (Postgres queue)
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as ready, min(now()-created_at) as oldest_age from queue_jobs where state='ready';"
ready | oldest_age
-------+------------
48211 | 02:41:18
Significado: El backlog es grande; el trabajo más antiguo tiene casi 3 horas. Esto es un problema de throughput o de dependencia downstream.
Decisión: Escala consumidores con cuidado, pero primero confirma que la BD no es el cuello de botella. Considera pausar productores o aplicar retropresión aguas arriba.
Task 15: Inspect Redis keyspace and TTL behavior for sessions
cr0x@server:~$ redis-cli -h redis-01 INFO keyspace
# Keyspace
db0:keys=4123891,expires=4100022,avg_ttl=286000
Significado: Casi todas las claves expiran; TTL promedio ~286 segundos. Eso es típico para límites de tasa, arriesgado para sesiones a menos que sea intencional.
Decisión: Si son sesiones y el TTL es corto, estás generando churn y desconexiones. Ajusta la estrategia de TTL, usa tokens de refresh o mueve la autoridad a Postgres.
Task 16: Confirm Postgres queue claim behavior (SKIP LOCKED)
cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "begin; select id from queue_jobs where state='ready' order by id limit 3 for update skip locked; commit;"
BEGIN
id
------
9912
9913
9914
(3 rows)
COMMIT
Significado: Los workers pueden reclamar trabajos de forma segura sin doble procesamiento gracias a los locks de fila.
Decisión: Si esto bloquea o devuelve nada mientras existe backlog, investiga contención de locks, índices faltantes o trabajos atascados “in progress”.
Tres mini-historias corporativas desde las trincheras
1) Incidente causado por una suposición errónea: “Las sesiones son caché”
Una empresa SaaS mediana movió sesiones de Postgres a Redis para “reducir carga”. En papel fue limpio:
session_id → JSON blob, TTL 30 días. Las lecturas se hicieron más rápidas, los gráficos de la base de datos se veían mejor, todos se fueron a casa temprano.
Meses después, el tráfico subió y el nodo Redis recibió un aumento de memoria. Durante la ventana de mantenimiento, el nodo se reinició.
No era gran cosa, pensaron—la persistencia estaba “habilitada”. Técnicamente estaba. Snapshots cada 15 minutos.
El radio de impacto fue inmediato: usuarios desconectados, pero peor, algunos controles de seguridad estaban acoplados al payload de sesión.
Una parte de las flags de “MFA verificado recientemente” se perdió y re-disparó desafíos de MFA. Los tickets de soporte se amontonaron, demos de ventas se arruinaron,
y el incidente tomó ese tono especial donde todos están técnicamente calmados pero emocionalmente furiosos.
La suposición errónea no fue “Redis es poco fiable.” Redis estaba haciendo exactamente lo que se configuró para hacer.
La suposición errónea fue que las sesiones eran “caché reconstruible”. No lo eran. Las sesiones se habían convertido en un artefacto de derechos.
La solución fue aburrida y efectiva: los registros autoritativos de sesión volvieron a Postgres con una tabla de revocación.
Redis quedó, pero solo para enriquecimiento derivado de sesión con TTL. También cambiaron el runbook: cualquier reinicio de Redis se trata como un evento planeado que puede afectar la autenticación a menos que se demuestre lo contrario.
2) Optimización que salió mal: el script Lua limitador
Otra empresa operaba una API pública y hacía limitación de tasa en Redis. Inicialmente fue un INCR + EXPIRE sencillo.
Alguien lo mejoró con un script Lua que implementaba un log de ventana deslizante. La equidad mejoró, los dashboards se vieron mejor y el desarrollador fue elogiado.
Luego la API lanzó una funcionalidad por lotes. Los clientes golpearon el endpoint en ráfagas, que es lo que hacen las funciones por lotes.
El script Lua ahora corría sobre claves calientes con grandes sorted sets. Bajo carga, el script empezó a aparecer en SLOWLOG.
Redis, siendo single-threaded en la ejecución de comandos, se convirtió en el punto de estrangulamiento. La latencia subió, los clientes hicieron timeout, los clientes reintentaron,
y los reintentos aumentaron la carga. El limitador de tasa—pensado para proteger el sistema—se convirtió en el principal modo de fallo del sistema.
Intentaron escalar Redis verticalmente, lo que compró tiempo pero no paz. La solución real fue simplificar:
token bucket con operaciones atómicas y claves cortas; aceptar ligera injusticia en los bordes; añadir suavizado por tenant en la aplicación.
La equidad es agradable. La supervivencia es más agradable todavía.
Tras el incidente, añadieron una regla: cualquier cambio en scripts Lua requiere pruebas de carga y un plan de rollback.
El scripting en Redis es poderoso. También es la forma más fácil de introducir un lock global oculto en tu arquitectura.
3) Práctica aburrida pero correcta que salvó el día: outbox en Postgres + idempotencia
Una empresa procesaba pagos. Necesitaban emitir eventos “payment_succeeded” para disparar emails, provisión y analítica.
Usaban Postgres para datos core y tenían un sistema de workers separado. Alguien propuso empujar eventos directamente a Redis por velocidad.
El equipo que ya había sufrido insistió en una tabla outbox en Postgres: cuando una fila de pago se confirma, se inserta una fila en outbox
dentro de la misma transacción. Un worker background lee filas de outbox con SKIP LOCKED, publica a sistemas downstream y las marca como hechas.
No es glamoroso. Es extremadamente depurable.
Un día, un servicio downstream degradó y la publicación de eventos se enlenteció hasta casi detenerse. El backlog de outbox creció, pero los pagos continuaron seguros.
Al estar el outbox en Postgres, el equipo pudo consultar estados exactos atascados, reproducir con seguridad y ejecutar limpieza dirigida.
El detalle salvador fue la idempotencia: el outbox tenía una restricción única en (event_type, aggregate_id, version).
Cuando los workers reintentaban por timeout, los duplicados eran inofensivos. La restricción imponía el contrato incluso bajo caos.
El incidente terminó sin pérdida de datos, sin doble-provisión y sin una saga forense de una semana.
Nadie fue aplaudido por su arquitectura emocionante. Consiguieron algo mejor: una postmortem tranquila.
Errores comunes (síntomas → causa raíz → solución)
1) Desconexiones aleatorias y picos de “sesión no encontrada”
Síntomas: Usuarios desconectados intermitentemente; servicio de auth muestra cache misses; Redis con memoria cerca del máximo.
Causa raíz: La política de evicción de Redis permite evictar claves de sesión, o la ventana de persistencia pierde sesiones recientes tras el reinicio.
Solución: Mueve el estado autoritativo de sesiones a Postgres, o configura maxmemory-policy de Redis para evitar evictar claves de sesión y asegura que la persistencia coincide con tu tolerancia a pérdida.
2) Los límites de tasa se reinician tras deploy/restart
Síntomas: Ráfagas pasan por el limitador después de reiniciar Redis; un partner API se queja; caída súbita de 429 a cero.
Causa raíz: Estado del limitador en Redis volátil sin persistencia durable, o claves del limitador solo existen en memoria.
Solución: Decide si el reinicio es aceptable. Si no, usa AOF con fsync apropiado, o guarda contadores en Postgres para límites críticos, o implementa híbrido (camino rápido en Redis + reconciliación periódica).
3) El backlog de la cola crece mientras los workers parecen “saludables”
Síntomas: Workers corriendo, CPU baja, pero la edad de los trabajos aumenta. Redis blocked_clients alto o locks en Postgres presentes.
Causa raíz: Dependencia downstream lenta, timeout de visibilidad mal dimensionado, o workers atascados en un lock o transacción larga.
Solución: Instrumenta duración de trabajos y latencia de dependencias; implementa timeouts; en colas Postgres evita transacciones largas y separa claim+work de escrituras de negocio cuando sea posible.
4) La cola en Postgres causa contención de locks y enlentece toda la BD
Síntomas: Aumentan esperas de lock; latencia incrementada para consultas no relacionadas; autovacuum retrasado en la tabla de cola.
Causa raíz: Tabla de cola caliente con updates/deletes frecuentes; índices parciales faltantes; workers actualizando filas con demasiada frecuencia (heartbeats).
Solución: Usa índice parcial en “ready”; actualiza columnas mínimas; batch deletes; particiona por tiempo; considera mover colas efímeras de alto rendimiento a Redis streams.
5) La cola de Redis pierde trabajos cuando el consumidor falla
Síntomas: El trabajo desaparece tras la muerte del worker; no hay retry; proceso de negocio incompleto.
Causa raíz: Uso de pop simple sin ack/requeue (RPOP/BLPOP) y sin seguimiento de in-flight.
Solución: Usa BRPOPLPUSH con una lista de procesamiento y un reaper, o usa Redis streams con consumer groups, o mueve a una cola en Postgres con semántica de timeout de visibilidad.
6) “Demasiadas conexiones” en Postgres durante picos de login
Síntomas: Postgres rechaza conexiones; hilos de app se quedan; pgbouncer ausente o mal configurado.
Causa raíz: Un patrón de una conexión por petición; falta de pooling; escrituras de sesión en cada petición.
Solución: Añade pooling de conexiones, reduce frecuencia de escrituras (evita actualizar last_seen en cada petición) y precomput
a datos derivados de sesión en Redis si es seguro.
7) Picos de latencia en Redis cada pocos minutos
Síntomas: P99 sube; el limitador causa timeouts; los gráficos muestran picos periódicos.
Causa raíz: Fork para snapshot RDB, reescritura de AOF, o saturación de IO de disco si fsync de AOF es agresivo.
Solución: Ajusta el calendario de persistencia, usa fsync “everysec” si es aceptable, monitoriza tiempo de fork, asegura disco rápido y evita footprints de memoria enormes que hagan los forks costosos.
Listas de comprobación / plan paso a paso
Checklist A: decidir dónde van las sesiones
- Clasifica qué contiene una sesión: solo identidad, derechos (entitlements), estado de MFA o preferencias de usuario.
- Si en el payload hay entitlements/MFA, haz de Postgres la autoridad (o separa autoridad de caché).
- Define tolerancia a pérdida: “los usuarios pueden volver a iniciar sesión” vs “la revocación debe ser inmediata”.
- Elige estrategia de TTL: expiración absoluta + timeout por inactividad; decide quién refresca y cuándo.
- Planifica limpieza: ajuste de vacuum/autovacuum en Postgres; TTL y política de evicción en Redis.
- Prueba el comportamiento de reinicio en staging con carga realista y ajustes de persistencia.
Checklist B: implementar límites de tasa sin crear un nuevo cuello de botella
- Elige el algoritmo: token bucket suele ser el mejor compromiso.
- Define alcance: por IP, por usuario, por tenant, por endpoint—evita cardinalidad ilimitada sin plan.
- Usa Redis si necesitas alto rendimiento y puedes tolerar ventanas cortas de pérdida.
- Si los límites son contractuales (APIs partners), considera límites respaldados en Postgres o Redis con configuración durable.
- Instrumenta la latencia del limitador por separado; trátalo como una dependencia.
- Tener un “modo degradado”: permitir pequeños picos, bloquear endpoints caros y registrar decisiones para forense.
Checklist C: elegir una implementación de cola que no te traicione
- Escribe el contrato: at-least-once, política de reintentos, dead-letter, necesidades de orden, programación de delays, timeout de visibilidad.
- Si los trabajos deben acoplarse transaccionalmente a escrituras DB, usa patrón outbox en Postgres.
- Si el throughput es alto y los trabajos son efímeros, Redis streams puede ser una opción sólida—opréralo seriamente.
- Diseña claves de idempotencia y aplícalas (restricciones únicas en Postgres o sets de deduplicado en Redis con TTL).
- Haz la retropresión explícita: los productores deben reducir la velocidad cuando los consumidores van detrás.
- Construye consultas operativas: “trabajo más antiguo”, “atascado en progreso”, “distribución de contadores de reintento”.
Plan paso a paso de migración (de “lo que sea” a cuerdo)
- Inventario de estado: sesiones, límites de tasa, colas, claves de idempotencia. Para cada uno, define tolerancia a pérdida y necesidades de auditoría.
- Elige almacenes de autoridad: Postgres para la verdad durable; Redis para aceleración derivada/efímera.
- Añade observabilidad: latencia P95/P99 para llamadas Redis/Postgres, lag de cola, recuentos de evicción, retraso de autovacuum.
- Implementa dual-write solo si puedes reconciliar; prefiere cortes gradualess y estrategias de fallback de lectura.
- Prueba la “ruta de incidente”: reinicia Redis, fordea Postgres, simula partición de red, limita IO de disco.
- Escribe runbooks: qué hacer ante evicción, lag de replicación, tormentas de locks, picos de backlog.
Preguntas frecuentes
1) ¿Puedo almacenar sesiones en Redis de forma segura?
Sí, si “seguro” significa que aceptas ventanas de pérdida, riesgo de evicción y comportamiento tras reinicios—o configuras persistencia y capacidad acorde a tus requisitos.
Si las sesiones son artefactos de seguridad autoritativos, mantén la autoridad en Postgres y cachea datos derivados en Redis.
2) ¿La persistencia de Redis (AOF/RDB) es suficiente para tratarlo como base de datos?
A veces, pero no lo banalices. Los ajustes de persistencia determinan ventanas de pérdida y rendimiento. Los snapshots basados en fork y la reescritura de AOF tienen costes operativos.
Si el negocio no puede tolerar pérdida, Postgres suele ser la historia de durabilidad más simple.
3) ¿Por qué no usar Postgres para todo?
Puedes, y muchos equipos deberían hacerlo más. Los límites son throughput, latencia y contención bajo contadores o colas extremadamente calientes.
Redis encaja mejor cuando necesitas operaciones atómicas a muy alta tasa con semánticas TTL.
4) ¿Por qué no usar Redis para todo?
Porque “en memoria” no significa “infalible”, y porque la capacidad de consulta importa durante incidentes y auditorías.
Redis es increíble en ciertos patrones; no es un sistema de registro general a menos que lo diseñes así y aceptes los tradeoffs.
5) ¿Son los streams de Redis una cola real?
Están más cerca que las listas o pub/sub: obtienes consumer groups, acknowledgments y entradas pendientes que puedes inspeccionar.
Aún necesitas diseñar reintentos, dead-letter y herramientas operativas. Streams son un conjunto de herramientas de cola, no un producto de cola completo.
6) ¿Cómo implemento una cola fiable en Postgres?
Usa una tabla de jobs con un “state” y timestamps, reclama trabajos vía FOR UPDATE SKIP LOCKED, mantén transacciones cortas e implementa lógica de retry/dead-letter.
Si el enqueue debe acoplarse a escrituras de negocio, usa un patrón outbox en la misma transacción.
7) ¿Cuál es el mayor riesgo operativo con Postgres para sesiones?
El churn y el bloat. Las tablas de sesiones generan tuplas muertas rápidamente. Si autovacuum no está ajustado, la latencia se vuelve impredecible. Particionar y reducir frecuencia de escrituras ayuda mucho.
8) ¿Cuál es el mayor riesgo operativo con Redis para limitación de tasa?
Convertir el limitador en un cuello de botella. Scripts Lua pesados, claves de alta cardinalidad y picos de latencia inducidos por persistencia pueden crear un bucle de fallo
donde los timeouts generan reintentos, que generan más tráfico al limitador.
9) ¿Deben ser durables los límites de tasa?
Depende de qué protegen. Si es equidad interna, los reinicios suelen ser aceptables. Si es contractual (APIs partners, controles antifraude),
la durabilidad importa—ya sea vía configuración persistente de Redis o una autoridad respaldada por Postgres.
10) ¿Cómo evito el doble procesamiento de trabajos?
Asume entrega al menos una vez y haz handlers idempotentes. En Postgres, aplica idempotencia con restricciones únicas.
En Redis, usa claves de deduplicado (con TTL) o incrusta idempotencia en las escrituras downstream.
Próximos pasos que puedes ejecutar esta semana
Deja de discutir qué herramienta es “mejor”. Decide qué fallos puedes tolerar, luego elige el datastore que falle de formas aceptables.
Redis es fantástico para velocidad y estado impulsado por TTL. Postgres es fantástico para la verdad, el acoplamiento y la claridad forense.
La mayoría de sistemas resilientes usan ambos, con un límite estricto entre autoridad y aceleración.
- Escribe qué sucede si Redis pierde todas las claves. Si la respuesta incluye “incidente de seguridad”, mueve la autoridad a Postgres.
- Ejecuta las tareas prácticas arriba en staging y producción. Captura líneas base de latencia, evicción, esperas de locks y salud de vacuum.
- Para colas, elige un contrato (at-least-once + idempotencia es el predeterminado sensato) e implementa timeouts de visibilidad y dead-lettering.
- Para sesiones, separa autoridad de caché: Postgres para revocación/expiración; Redis para payload derivado y recomputable.
- Para límites de tasa, mantén el algoritmo simple, instrumenta el limitador y trátalo como una dependencia que puede tumbarte.
Almacénalo correctamente ahora, o paga después. La factura llega durante el incidente. Siempre llega.