Si alguna vez tuviste una conversación tipo “pero la documentación decía que era ACID” mientras un pager vibraba sobre la mesa,
ya conoces el tema: las transacciones no son una característica, son un comportamiento del sistema. Y el comportamiento del sistema
tiene bordes afilados—especialmente cuando la replicación, el failover, las cachés y los reintentos de la aplicación se mezclan.
PostgreSQL y MongoDB ambos soportan transacciones. Ambos tienen perillas de aislamiento. Ambos dicen ofrecer durabilidad.
En producción, la diferencia es menos sobre “quién tiene transacciones” y más sobre lo que realmente obtienes
cuando la red titubea, el primario pierde el rol, el disco se queda atascado y tu app hace lo menos útil: reintentar.
Dónde la documentación termina y comienza la realidad
La documentación suele responder “qué se soporta”. Producción pregunta “qué pasa cuando falla”.
Las transacciones no son solo BEGIN/COMMIT; son un contrato entre:
el motor de almacenamiento, el protocolo de replicación, el comportamiento de failover, los controladores cliente y tu lógica de reintentos.
El modelo de transacciones de PostgreSQL es más antiguo, más estricto y opinado. Está construido alrededor de MVCC y WAL,
y tiende a fallar de maneras reconocibles: contención por locks, bloat, lag de replicación, conflictos en hot standby.
La historia transaccional de MongoDB llegó después y se superpone a un replica set y a un motor de documentos.
Puede ser perfectamente correcta—hasta que mezclas write concerns, stepdowns, transacciones de larga duración
y patrones de carga que chocan con su diseño.
Aquí está la verdad operativa: si tu sistema necesita invariantes entre múltiples entidades, unicidad correcta y
rollback predecible bajo concurrencia, PostgreSQL es la opción por defecto. MongoDB puede hacer transacciones,
pero las pagas, y debes ser explícito sobre durabilidad y semánticas de lectura.
Si tratas “soporta transacciones” como “funciona como Postgres”, estarás construyendo una trampa para tu yo futuro.
Breve historia: por qué estos sistemas se comportan así
Las transacciones no son un accesorio de moda. Son el producto de décadas de dolor. Unos puntos de contexto que
explican los tradeoffs actuales:
- PostgreSQL desciende de POSTGRES (finales de 1980s), diseñado en una era académica donde la corrección era objetivo de primera clase, no un SKU opcional.
- El MVCC de PostgreSQL se convirtió en su estrategia de concurrencia definitoria: los lectores no bloquean a los escritores, pero el vacuum se vuelve tu compañero silencioso que consume CPU por la noche.
- WAL (Write-Ahead Logging) es la columna vertebral de la durabilidad en Postgres; por eso la recuperación tras crash es aburrida de la mejor manera.
- MongoDB empezó (finales de 2000s) enfatizando agilidad para desarrolladores y flexibilidad de documentos; las versiones tempranas se apoyaban fuertemente en atomicidad a nivel de documento.
- Los replica sets se volvieron el centro operativo de MongoDB; “primary” y “secondaries” no son solo topología, definen semánticas de lectura/escritura.
- Las transacciones multi-documento de MongoDB llegaron más tarde (4.0+), primero para replica sets y luego para clusters sharded—implementadas coordinando escrituras con trabajo adicional de bookkeeping.
- “Majority write concern” existe porque la replicación asíncrona no es durabilidad; es optimismo con buen marketing.
- El aislamiento por defecto de PostgreSQL es Read Committed, lo cual sorprende a desarrolladores que esperan un comportamiento serializable por defecto.
- Los niveles de read concern de MongoDB evolucionaron para abordar “qué fue lo que leí realmente” en un mundo replicado (local, majority, snapshot, linearizable).
Estas historias importan porque predicen los modos de fallo. Postgres tiende a priorizar la corrección primero,
luego te hace afinar el rendimiento. MongoDB tiende a facilitar escalado y evolución de esquema,
luego te hace elegir cuánta corrección estás dispuesto a pagar en tiempo de ejecución.
Modelo de transacciones de PostgreSQL: MVCC, locks y WAL
MVCC significa “las lecturas ven una instantánea”, no “los locks no existen”
PostgreSQL usa MVCC: cada versión de fila tiene metadatos de visibilidad, y una transacción ve una instantánea de lo que está
comprometido (en un cierto punto, dependiendo del aislamiento). Las lecturas no bloquean escrituras porque los lectores leen versiones
antiguas de filas. Los escritores crean nuevas versiones. Esto es elegante, pero tiene consecuencias:
tuplas muertas, requisitos de vacuum y transacciones de larga duración que impiden la limpieza.
El bloqueo sigue importando. Actualizar una fila toma locks a nivel de fila. DDL toma locks más pesados.
La unicidad se hace cumplir con un comportamiento de locking a nivel de índice que es seguro pero puede sorprender bajo claves calientes.
Y si empujas la concurrencia mientras mantienes locks demasiado tiempo, Postgres te mostrará pacientemente una cola de miseria.
Niveles de aislamiento en la práctica
PostgreSQL soporta Read Committed, Repeatable Read y Serializable.
En producción, la mayoría de sistemas ejecutan Read Committed y confían en restricciones y consultas cuidadosas.
Serializable es real, pero es optimista: puede abortar transacciones con fallos de serialización bajo contención.
Eso no es un bug; es la base de datos diciéndote “tu historia de concurrencia no es serializable”.
El momento “la realidad difiere de la documentación” ocurre cuando un equipo activa Serializable sin implementar reintentos para
SQLSTATE 40001. Entonces aprenden que la corrección requiere cooperación.
Durabilidad WAL: COMMIT es una historia de disco, no de sentimientos
En PostgreSQL, la durabilidad es fundamentalmente sobre que el WAL se haya vaciado a disco. Las perillas son
synchronous_commit, wal_sync_method, y el comportamiento del sistema de archivos y almacenamiento subyacente.
La replicación introduce otra capa: ¿el commit es reconocido solo por el primario, o también por réplicas?
Cuando alguien dice “Postgres perdió datos comprometidos”, suele ser uno de:
failover de replicación asíncrona, durabilidad relajada deliberadamente (synchronous_commit=off),
una pila de almacenamiento que mintió (caché de escritura sin barreras adecuadas), o error humano.
La superpotencia de Postgres: las restricciones se hacen cumplir transaccionalmente
Foreign keys, unique constraints, exclusion constraints, check constraints. Puedes modelar invariantes cerca
de los datos y confiar en ellas bajo carga concurrente. Esto no es solo conveniencia; reduce la superficie
de condiciones de carrera a nivel de aplicación.
Modelo de transacciones de MongoDB: sesiones, write concern y replica sets
Las transacciones de MongoDB existen—pero no son gratuitas
Las transacciones multi-documento de MongoDB (replica set y sharded) proporcionan semánticas ACID dentro de la transacción.
El “dentro” hace mucho trabajo aquí. El motor mantiene estado adicional, y el coordinador debe gestionar
el commit entre participantes (especialmente en despliegues sharded). La latencia sube. El throughput suele bajar.
Bajo contención, puedes ver más abortos y errores transitorios que requieren reintentos.
Si tu carga en MongoDB es mayormente operaciones de un solo documento, Mongo puede ser extremadamente rápido y agradable operativamente.
El momento en que te apoyas fuertemente en transacciones multi-documento para la corrección central, estás pidiendo a MongoDB
que se comporte como una base de datos relacional—mientras sigues pagando el impuesto de ser una tienda de documentos distribuida.
Write concern es la diferencia entre “reconocido” y “suficientemente durable”
El write concern de MongoDB define qué significa “éxito” para una escritura. La trampa clásica: usar
w:1 (reconocido por el primario) y asumir que significa que la escritura sobrevivirá a un failover.
Puede que sí. También puede que no, dependiendo del timing y la replicación.
Para durabilidad frente a la falla del primario, típicamente quieres w:majority más ajustes sensatos de journaling
(journaling está habilitado por defecto en versiones modernas, pero confírmalo). Luego aprendes la siguiente realidad:
majority puede ser más lento, especialmente con secondaries lentos o despliegues inter-zonas.
Read concern: ¿qué fue lo que realmente leí?
El read concern de MongoDB controla las garantías de visibilidad: local, majority,
snapshot y linearizable (con restricciones). Read preference añade otro eje:
lecturas al primario vs a segundos.
En producción, el error común es leer de secondaries para “escalar” mientras las escrituras quedan en el primario,
y luego sorprenderse cuando los usuarios ven datos “regresar” o cuando una transacción lee algo que luego
desaparece tras un rollback. Si tratas el lag de replicación como una característica, tu corrección se volverá opcional.
Sesiones, reintentos y el impuesto del “resultado de commit desconocido”
Los drivers de MongoDB implementan escrituras reintentables y comportamiento de reintento de transacciones. Bien—hasta que no.
Puedes tener casos donde el cliente hace timeout y no sabe si el commit tuvo éxito.
Entonces tu aplicación reintenta y crea duplicados accidentalmente a menos que diseñes idempotencia.
Eso no es que MongoDB sea malvado; son sistemas distribuidos siendo sistemas distribuidos. Pero necesitas planear para ello.
Semánticas que importan: una matriz comparativa práctica
1) Alcance de atomicidad
PostgreSQL: atómico a través de cualquier fila/tablas tocadas por la transacción dentro de la base de datos.
MongoDB: atómico por defecto a nivel de documento único; la atomicidad multi-documento existe vía transacciones, con overhead
y más advertencias operativas (especialmente entre shards).
2) Valores por defecto de aislamiento y sorpresa para desarrolladores
PostgreSQL por defecto Read Committed: cada statement ve una instantánea al inicio de la sentencia. Muchas anomalías se previenen
con restricciones y estructura de consultas cuidadosa, pero aún puedes escribir carreras si asumes comportamiento “repeatable”.
MongoDB: fuera de transacciones, estás mayormente en un mundo por-operación con read concern y write concern.
Dentro de transacciones, puedes obtener comportamiento de snapshot isolation. La sorpresa suele ser sobre lo que “snapshot” significa
relativo a la replicación y read preference.
3) Semánticas de durabilidad bajo failover
PostgreSQL commit solo en el primario es durable en ese nodo una vez que el WAL está vaciado (salvo honestidad del almacenamiento).
Pero si haces failover a una réplica asíncrona, puedes perder transacciones reconocidas que no se replicaron.
MongoDB: una escritura reconocida con w:1 puede hacer rollback en un failover. Con w:majority,
el riesgo de rollback se reduce significativamente porque la escritura fue replicada a una mayoría.
4) “Lee tus propias escrituras” y lecturas monótonas
PostgreSQL en un solo nodo: sí, dentro de la misma transacción y sesión. Con réplicas, depende de tu enrutamiento; si lees desde réplicas puedes ver lag.
MongoDB: si lees desde secondaries sin sesiones causalmente consistentes (y ajustes correctos), puedes leer datos obsoletos.
Incluso con sesiones, los cambios de topología pueden complicar las garantías. El sistema puede ser correcto; tus suposiciones podrían no serlo.
5) Observabilidad
PostgreSQL expone el estado transaccional y de locking muy directamente vía catálogos y vistas del sistema.
MongoDB expone estado vía serverStatus/currentOp, profiler y métricas de replicación.
Ambos son observables. Postgres tiende a darte imágenes más claras de “quién bloquea a quién”; MongoDB suele hacerte
razonar sobre salud del replica set y cumplimiento del write concern.
6) Radio de impacto operativo
Postgres: una consulta mala puede bloquear una tabla, inflar almacenamiento o saturar I/O; las fallas suelen localizarse en ese nodo,
con lag de replicación como síntoma secundario.
MongoDB: un miembro enfermo del replica set, secondaries lentos o elecciones pueden convertir “escrituras rápidas” en “por qué todo está timeout”.
En clusters sharded, las transacciones entre shards pueden propagar el dolor rápidamente.
Modos de fallo que encuentras a las 2 a.m.
PostgreSQL: contención por locks y “¿por qué COMMIT es lento?”
La latencia de COMMIT en Postgres suele ser latencia de almacenamiento (fsync), contención de WAL, o espera por replicación síncrona.
Rara vez es “Postgres está lento” en abstracto. Casi siempre es “tu almacenamiento o tus ajustes de durabilidad están haciendo
que tus semánticas deseadas sean caras.”
Otro clásico: una transacción de larga duración impide que vacuum limpie tuplas muertas, infla índices,
y entonces todo se vuelve lento en una forma que parece “colapso de I/O aleatorio”.
MongoDB: elecciones, esperas por write concern y reintentos de transacciones
En MongoDB, el dolor operativo a menudo proviene del replica set haciendo su trabajo:
ocurren elecciones, los primarios renuncian al rol, y los clientes ven errores transitorios. Si tu aplicación no maneja esto bien,
tu incidente se convierte en “la base de datos está caída” cuando en realidad es “tus reintentos son un DDoS contra tu propio primario.”
Las transacciones pueden amplificar esto. Una transacción abierta mientras hace mucho trabajo mantiene recursos ocupados
y aumenta la probabilidad de que algo cambie debajo de ti (como un stepdown), forzando un abort y reintento.
La verdad distribuida: no puedes evitar la ambigüedad
Ambos sistemas pueden dejarte en la ambigüedad “¿se comprometió o no?” cuando el cliente pierde la respuesta.
Postgres típicamente te deja implementar idempotencia en la capa de aplicación.
MongoDB hace el problema más explícito con escrituras reintentables y comportamiento de commit de transacciones—pero aún debes diseñar
para ello.
Una cita que vale la pena colgar en la pared, porque se mantiene cierta sin importar qué base elijas:
La esperanza no es una estrategia.
—Gene Kranz
Broma #1: Una transacción es como una promesa: reconforta hasta que tienes que hacerla cumplir en un tribunal, es decir, en producción.
Tres microhistorias del mundo corporativo
Incidente: una suposición errónea sobre “escrituras reconocidas”
Una compañía SaaS de tamaño medio usaba MongoDB para perfiles de usuario y estado de facturación. El esquema estaba limpio, el código era moderno,
y el equipo tenía la costumbre de leer “acknowledged” como “durable”. Las escrituras usaban el write concern por defecto, y la app
leía desde el primario. Todo parecía bien en staging, y bien durante meses en producción.
Entonces una ventana de mantenimiento rutinaria coincidió con un flap de red entre zonas de disponibilidad. El primario aceptó escrituras,
las reconoció, y poco después perdió el rol durante una elección. Algunas de esas escrituras de último minuto no se habían replicado
a una mayoría. Se eligió un nuevo primario que no las tenía.
El incidente no fue un outage dramático. Fue peor: unas pocas acciones de clientes “desaparecieron”. Llegaron tickets de soporte
con capturas de pantalla. Ingeniería inicialmente persiguió fantasmas en el frontend porque los logs del backend mostraban escrituras exitosas.
Los datos simplemente ya no estaban.
La solución fue operacional y arquitectónica. Movieron escrituras críticas a w:majority (con timeouts ajustados),
hicieron que ciertas lecturas usaran readConcern: "majority" al devolver estado crítico de facturación, y añadieron claves de idempotencia
para que los reintentos no duplicaran efectos colaterales. El rendimiento bajó ligeramente; la corrección mejoró masivamente.
Optimización que salió mal: “vamos a acelerar commits de Postgres”
Un equipo fintech ejecutaba PostgreSQL con un ledger muy intensivo en escrituras. Perseguían una regresión de latencia p99 y notaron que
la latencia de commit se correlacionaba con picos de I/O. Alguien propuso el clásico ajuste:
poner synchronous_commit=off para escrituras “no críticas” y confiar en replicación.
Funcionó. La latencia mejoró al instante. El throughput aumentó. Los gráficos parecían el currículum perfecto.
Entonces tuvieron un evento de energía no limpio en el primario. La máquina se reinició. El WAL no se había vaciado para algunos commits.
La base de datos recuperó correctamente—es decir, revirtió esas transacciones porque, según la nueva configuración, nunca
estaban garantizadas como durables.
El impacto en el negocio no fue catastrófico, pero fue humillante: acciones “confirmadas” tuvieron que reconciliarse.
El equipo aprendió que los atajos de durabilidad no son gratis; son consecuencias aplazadas.
Revirtieron la configuración, se mudaron a almacenamiento más rápido para WAL, y usaron procesamiento asíncrono para eventos verdaderamente no críticos
en lugar de debilitar la durabilidad en el ledger principal.
Broma #2: Apagar la durabilidad para hacer commits más rápidos es como quitar el detector de humo porque pita demasiado fuerte.
Aburrido pero correcto: diseño centrado en restricciones que salvó el día
Una plataforma B2B almacenaba órdenes en PostgreSQL y tenía un worker en background que “finalizaba” órdenes creando facturas,
decrementando inventario y enviando emails. El flujo estaba distribuido entre servicios, como era de esperar.
El equipo hizo algo poco a la moda: modelaron invariantes en la base de datos. Los decrementos de inventario estaban restringidos a
no bajar de cero. Los números de factura eran únicos con un índice estricto. La máquina de estados de la orden estaba protegida por check constraints
y las transiciones se imponían en SQL transaccional.
Durante un deploy, un bug hizo que el worker se ejecutara dos veces para un subconjunto de órdenes. En muchos sistemas eso se vuelve un proyecto
de limpieza de varios días. Aquí, la segunda ejecución falló rápido por violaciones de restricciones.
El servicio registró errores, SREs vieron el pico, y los clientes mayormente no se enteraron.
El postmortem fue corto y casi alegre: la base de datos había actuado como un disyuntor. La solución fue corregir el bug de programación del job
y mejorar la idempotencia, pero las restricciones ya habían impedido corrupción financiera y de inventario.
Lo aburrido puede ser hermoso.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son los chequeos que ejecuto cuando alguien dice “las transacciones son lentas” o “perdimos datos” o “está inconsistente”.
Cada tarea incluye: un comando, un fragmento de salida realista, qué significa y qué decisión tomar a partir de ello.
Tarea 1 (PostgreSQL): Confirmar ajustes de aislamiento y durabilidad
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW default_transaction_isolation; SHOW synchronous_commit; SHOW wal_level;"
default_transaction_isolation
-----------------------------
read committed
(1 row)
synchronous_commit
--------------------
on
(1 row)
wal_level
-----------
replica
(1 row)
Significado: Read Committed por defecto; los commits esperan el vaciado de WAL; WAL configurado para replicación.
Decisión: Si estás depurando anomalías, confirma si la app asume Repeatable Read/Serializable.
Si la latencia de commit es alta, mantén synchronous_commit=on a menos que aceptes explícitamente pérdida de datos.
Tarea 2 (PostgreSQL): Identificar quién bloquea a quién
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_locks blocked_locks ON blocked_locks.pid = blocked.pid JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid JOIN pg_stat_activity blocking ON blocking.pid = blocking_locks.pid WHERE NOT blocked_locks.granted;"
blocked_pid | blocked_query | blocking_pid | blocking_query
------------+------------------------------+--------------+-------------------------------
24190 | UPDATE orders SET ... | 23811 | ALTER TABLE orders ADD COLUMN
(1 row)
Significado: Un DDL está bloqueando una escritura. Eso no es “las transacciones son lentas”, es “alguien tomó un lock pesado”.
Decisión: Detén el DDL si es inseguro, o prográmalo adecuadamente. Usa builds de índice concurrentes y patrones de migración online.
Tarea 3 (PostgreSQL): Buscar transacciones de larga duración que impidan vacuum
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT pid, now() - xact_start AS xact_age, state, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;"
pid | xact_age | state | query
-------+------------+--------+----------------------------------------
31245 | 02:13:08 | idle | BEGIN;
29902 | 00:18:41 | active | SELECT ... FROM events ORDER BY ...
(2 rows)
Significado: Una transacción idle ha estado abierta por horas. Eso fija versiones antiguas de fila y puede causar bloat.
Decisión: Arregla la aplicación: asegura que las transacciones sean cortas y siempre se hagan commit/rollback.
Considera idle_in_transaction_session_timeout.
Tarea 4 (PostgreSQL): Revisar presión de WAL y checkpoints
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time FROM pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time
------------------+-----------------+-----------------------+----------------------
1021 | 487 | 98765432 | 1234567
(1 row)
Significado: Muchos checkpoints solicitados; el tiempo de escritura es alto. La presión de checkpoints puede afectar la latencia de commit y el I/O.
Decisión: Ajusta max_wal_size, checkpoint_timeout, y asegura que el almacenamiento pueda manejar WAL y escrituras de datos.
Tarea 5 (PostgreSQL): Medir lag de replicación y ventana de riesgo
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
application_name | state | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
pg-replica-1 | streaming | async | 00:00:02 | 00:00:03 | 00:00:05
(1 row)
Significado: La réplica es async y está segundos detrás. Un failover puede perder unos segundos de commits.
Decisión: Si no puedes tolerar eso, implementa replicación síncrona para clústeres críticos,
o cambia la política de failover para evitar promover réplicas con lag.
Tarea 6 (PostgreSQL): Detectar fallos de serialización y necesidad de reintentos
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT datname, xact_commit, xact_rollback, conflicts FROM pg_stat_database WHERE datname='app';"
datname | xact_commit | xact_rollback | conflicts
---------+-------------+---------------+-----------
app | 98765432 | 123456 | 842
(1 row)
Significado: Existen rollbacks/conflictos. No todos los rollbacks son malos, pero picos pueden indicar fallos de serialización o deadlocks.
Decisión: Correlaciona con errores de aplicación (SQLSTATE 40001, 40P01). Añade lógica de reintentos con jitter y reduce hotspots de contención.
Tarea 7 (MongoDB): Inspeccionar salud del replica set y churn de elecciones
cr0x@server:~$ mongosh --host rs0/mb-primary,mb-secondary-1,mb-secondary-2 --eval "rs.status().members.map(m=>({name:m.name,stateStr:m.stateStr,health:m.health,optime:m.optime.ts}))"
[
{ name: 'mb-primary:27017', stateStr: 'PRIMARY', health: 1, optime: Timestamp({ t: 1735550101, i: 1 }) },
{ name: 'mb-secondary-1:27017', stateStr: 'SECONDARY', health: 1, optime: Timestamp({ t: 1735550101, i: 1 }) },
{ name: 'mb-secondary-2:27017', stateStr: 'SECONDARY', health: 1, optime: Timestamp({ t: 1735550096, i: 1 }) }
]
Significado: Un secondary está unos segundos retrasado. Eso importa para la latencia de w:majority y la obsolescencia de lecturas desde segundos.
Decisión: Si las escrituras majority son lentas, investiga miembros con lag; si se usan lecturas desde secondaries, considera read concerns más estrictos o enrutamiento.
Tarea 8 (MongoDB): Verificar defaults de write concern y journaling
cr0x@server:~$ mongosh --host mb-primary --eval "db.getMongo().getWriteConcern()"
{ w: 1 }
Significado: El por defecto es w:1. Eso es “reconocido por el primario”, no “sobrevive a pérdida del primario”.
Decisión: Para datos críticos, ajusta w:majority en el cliente o a nivel de colección, y usa timeouts para evitar escrituras colgadas.
Tarea 9 (MongoDB): Comprobar abortos de transacción y tormentas de reintentos
cr0x@server:~$ mongosh --host mb-primary --eval "db.serverStatus().transactions"
{
currentActive: Long('14'),
currentInactive: Long('3'),
currentOpen: Long('17'),
totalAborted: Long('982'),
totalCommitted: Long('184433'),
totalStarted: Long('185415')
}
Significado: Están ocurriendo abortos. Algunos abortos son normales; picos durante stepdowns o contención no lo son.
Decisión: Si los abortos suben, acorta transacciones, reduce trabajo entre colecciones, y confirma que el comportamiento de reintentos del driver no esté amplificando la carga.
Tarea 10 (MongoDB): Encontrar operaciones lentas que mantienen transacciones abiertas
cr0x@server:~$ mongosh --host mb-primary --eval "db.currentOp({active:true, secs_running:{$gte:5}}).inprog.map(op=>({secs:op.secs_running,ns:op.ns,desc:op.desc,command:op.command && Object.keys(op.command)}))"
[
{ secs: 31, ns: 'app.orders', desc: 'conn12345', command: [ 'find' ] },
{ secs: 12, ns: 'app.inventory', desc: 'conn23456', command: [ 'update' ] }
]
Significado: Operaciones activas corriendo largo tiempo. En transacciones, las operaciones largas aumentan probabilidad de conflicto y uso de recursos.
Decisión: Añade índices, reduce el tamaño de los escaneos de documentos y limita el trabajo por transacción. Si es una consulta fuera de control, mátala deliberadamente.
Tarea 11 (MongoDB): Confirmar read preference y read concern en la ruta cliente
cr0x@server:~$ mongosh --host mb-primary --eval "db.getMongo().getReadPref()"
{ mode: 'secondaryPreferred' }
Significado: Las lecturas pueden ir a secondaries. Eso es una elección de corrección, no un truco de escala gratuito.
Decisión: Para flujos de usuario “acabo de escribir esto”, usa lecturas al primario o sesiones con consistencia causal y readConcern apropiado.
Tarea 12 (PostgreSQL): Confirmar comportamiento de fsync y detectar “almacenamiento que miente” temprano
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW fsync; SHOW full_page_writes; SHOW wal_log_hints;"
fsync
-------
on
(1 row)
full_page_writes
------------------
on
(1 row)
wal_log_hints
---------------
off
(1 row)
Significado: WAL se está fsynceando; full page writes están activadas (importante para seguridad contra crashes).
Decisión: Mantén estos ajustes en producción a menos que tengas una razón muy específica y controles compensatorios.
Si sospechas de la pila de almacenamiento, valida los ajustes de caché en el nivel hardware como parte de la respuesta al incidente.
Tarea 13 (PostgreSQL): Encontrar deadlocks y las sentencias que los causaron
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT deadlocks, conflicts FROM pg_stat_database WHERE datname='app';"
deadlocks | conflicts
-----------+-----------
19 | 842
(1 row)
Significado: Ocurrieron deadlocks. Postgres matará a un participante; tu app verá un error y debe reintentar.
Decisión: Identifica tablas/consultas (vía logs), estandariza el orden de locks en el código y mantén transacciones pequeñas.
Tarea 14 (MongoDB): Comprobar lag de replicación en segundos, no en sensaciones
cr0x@server:~$ mongosh --host mb-primary --eval "rs.printSecondaryReplicationInfo()"
source: mb-secondary-1:27017
syncedTo: Mon Dec 30 2025 02:41:55 GMT+0000 (UTC)
0 secs (0 hrs) behind the primary
source: mb-secondary-2:27017
syncedTo: Mon Dec 30 2025 02:41:49 GMT+0000 (UTC)
6 secs (0 hrs) behind the primary
Significado: Un secondary está 6 segundos atrás. Eso afecta la velocidad de reconocimiento majority y lecturas obsoletas.
Decisión: Investiga ese nodo (disco, CPU, red). Si rutinariamente está atrasado, perseguirá tus latencias de transacción.
Tarea 15 (PostgreSQL): Confirmar ajustes de replicación síncrona si se requiere “sin pérdida de datos”
cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW synchronous_standby_names; SHOW synchronous_commit;"
synchronous_standby_names
--------------------------
'ANY 1 (pg-replica-1)'
(1 row)
synchronous_commit
--------------------
on
(1 row)
Significado: El primario espera por al menos una standby síncrona. Así reduces sorpresas de “reconocido pero perdido”.
Decisión: Usa esto para sistemas críticos, pero monitoriza: enlaza la latencia de escritura a la salud de réplicas y la calidad de red.
Guion rápido de diagnóstico
Cuando el comportamiento transaccional es “raro”, no empieces debatiendo bases de datos. Empieza por localizar el cuello de botella.
Este es el orden que te lleva a la causa raíz antes de que llegue la invitación a la reunión.
Primero: determina si el dolor es latencia, throughput o corrección
- Pico de latencia: commits lentos, consultas lentas, timeouts.
- Caída de throughput: crecimiento de colas, backlog de workers, conexiones en aumento.
- Bug de corrección: escrituras faltantes, duplicados, lecturas fuera de orden.
Segundo: revisa el “contrato de durabilidad y replicación”
- PostgreSQL: ¿la replicación es asíncrona? ¿ocurrió un failover? ¿se relajó
synchronous_commit? ¿el vaciado de WAL es lento? - MongoDB: ¿cuál es el write concern? ¿están ocurriendo elecciones? ¿los secondaries están laggeando? ¿los clientes leen desde secondaries?
Tercero: comprueba la contención
- PostgreSQL: locks bloqueantes, deadlocks, transacciones de larga duración, hot rows, contención en índices.
- MongoDB: conflictos de transacción, ops de larga duración, esperas por write concern, overhead del coordinador de shards.
Cuarto: revisa saturación de almacenamiento y host
- La latencia de disco es la mano invisible detrás de “COMMIT es lento”.
- La saturación de CPU puede parecer “locks” porque todo se ralentiza y se forman colas.
- La jitter de red puede parecer “inestabilidad de la base” porque la replicación y elecciones son sensibles a timeouts.
Quinto: confirma el comportamiento del cliente
- ¿Estás reintentando en cada error sin backoff? Felicidades, construiste un multiplicador de outage.
- ¿Los timeouts son más cortos que tu camino de commit durante failover? Entonces generas “resultado de commit desconocido” por diseño.
- ¿Mezclas read preferences y esperas read-your-writes? Entonces estás probando tu suerte en producción.
Errores comunes: síntomas → causa raíz → solución
1) “Datos comprometidos desaparecieron después del failover”
Síntomas: logs de app muestran escrituras exitosas; después del failover, faltan datos.
Causa raíz: failover con replicación asíncrona (Postgres), o escrituras de MongoDB reconocidas con w:1 que fueron revertidas.
Solución: Para Postgres, usa replicación síncrona para datos críticos o evita promover réplicas con lag. Para MongoDB, usa w:majority y diseña idempotencia.
2) “Las transacciones son lentas solo en pico”
Síntomas: salto de latencia p95/p99; caída de throughput; CPU parece OK; usuarios hacen timeout.
Causa raíz: contención (locks en Postgres; esperas por write concern o conflictos de transacción en MongoDB), o saturación de fsync en almacenamiento.
Solución: Identifica cadenas bloqueantes / ops largas. Reduce el alcance de transacciones. Añade o corrige índices. Mueve WAL/journal a almacenamiento más rápido si es necesario.
3) “Habilitamos Serializable y todo empezó a fallar”
Síntomas: errores en Postgres por fallos de serialización; tormentas de reintentos.
Causa raíz: Serializable requiere reintentos; la alta contención dispara abortos.
Solución: Implementa reintentos acotados con jitter; reduce hotspots; considera Repeatable Read más restricciones si es apropiado.
4) “La transacción de MongoDB sigue abortando con errores transitorios”
Síntomas: alta tasa de abortos; stepdowns; la app ve errores transitorios de transacción.
Causa raíz: elecciones, transacciones de larga duración, conflictos o overhead por coordinación cross-shard.
Solución: Acorta transacciones, evita transacciones entre shards en paths críticos, ajusta timeouts y asegura que la lógica de reintentos del driver sea correcta y tenga límite de velocidad.
5) “Lecturas inconsistentes justo después de escrituras”
Síntomas: un usuario actualiza su perfil; al refrescar ve datos antiguos; luego aparecen.
Causa raíz: lectura desde réplicas/secondaries; read concern insuficiente; lag de réplicas.
Solución: Enruta lecturas read-your-writes al primario, o usa sesiones con consistencia causal y read concern apropiado; monitoriza lag de replicación.
6) “Uso de disco de Postgres sigue creciendo; consultas se degradan gradualmente”
Síntomas: bloat, I/O en ascenso, autovacuum no alcanza.
Causa raíz: transacciones largas y tablas con muchas actualizaciones que generan tuplas muertas; starvation de vacuum.
Solución: Elimina idle-in-transaction; ajusta autovacuum por tabla; considera particionado; programa mantenimiento cuando sea necesario.
7) “Usamos secondaryPreferred y ahora tenemos bugs ‘imposibles’”
Síntomas: contadores retroceden; transiciones de estado parecen fuera de orden.
Causa raíz: lecturas obsoletas desde secondaries; no se cumplen suposiciones sobre lecturas monótonas.
Solución: Usa lecturas al primario para máquinas de estado; si usas lecturas desde secondaries, acepta la obsolescencia explícitamente y diseña la UI/lógica en torno a ello.
8) “Nuestra lógica de reintentos empeoró el incidente”
Síntomas: pico de QPS y conexiones durante el outage; la BD se cae más fuerte.
Causa raíz: reintentos ilimitados, sin jitter, reintentos en errores no reintentables y falta de claves de idempotencia.
Solución: Implementa backoff exponencial acotado con jitter; diferencia errores reintentables; añade tokens de idempotencia; considera circuit breakers.
Listas de verificación / plan paso a paso
Lista de decisión: ¿debe este workload usar transacciones de Postgres o MongoDB?
- ¿Necesitas invariantes entre entidades? Si es así, favorece PostgreSQL. Si es MongoDB, espera usar transacciones multi-documento y pagarlas.
- ¿Necesitas garantías fuertes de unicidad entre muchos documentos? PostgreSQL es directo. MongoDB requiere indexado cuidadoso y diseño transaccional.
- ¿Es inaceptable la pérdida de datos en failover? Postgres: replicación síncrona y failover cuidadoso. MongoDB:
w:majorityy lecturas majority donde haga falta. - ¿Tu patrón de acceso es mayormente de un solo documento? MongoDB brilla; no lo conviertas accidentalmente en un sistema relacional mediante transacciones multi-documento constantes.
- ¿Los desarrolladores van a implementar reintentos de forma fiable? Si no, evita ajustes que los requieran frecuentemente (Serializable en todas partes; transacciones largas en MongoDB bajo contención).
- ¿Puedes contar con expertise operativo? Ambos lo requieren. La expertise en Postgres suele verse como afinación de consultas + vacuum + disciplina de replicación. La de MongoDB suele verse como higiene de replica set/shard + disciplina de write/read concern.
Plan de implementación: Postgres bien hecho para corrección transaccional
- Modela invariantes con restricciones (FKs, unique constraints, check constraints).
- Mantén las transacciones cortas; evita el patrón “transacción como workflow”.
- Implementa idempotencia para efectos colaterales externos (emails, pagos, webhooks).
- Elige el nivel de aislamiento intencionalmente; añade reintentos si usas Serializable.
- Decide tu consistencia en failover: async (posible pérdida) vs síncrono (mayor latencia).
- Instrumenta: esperas de locks, deadlocks, lag de replicación, tiempos de checkpoint, latencia de fsync en el host.
- Establece guardarraíles:
statement_timeout,idle_in_transaction_session_timeout, límites de pool de conexiones.
Plan de implementación: transacciones MongoDB sin sabotearte
- Por defecto usa patrones atómicos de documento único cuando sea posible (embed, operadores atómicos).
- Cuando necesites transacciones multi-documento, mantenlas cortas y con pocos documentos.
- Configura write concern explícitamente para escrituras críticas (
w:majority+ timeout). - Elige read concern y read preference intencionalmente; no mezcles “lecturas secundarias” con expectativas de corrección estricta.
- Implementa claves de idempotencia; diseña para el “resultado de commit desconocido”.
- Monitorea elecciones, lag de replicación, abortos de transacción y métricas de locks/colas.
- En clusters sharded, evita transacciones cross-shard en paths críticos si te gusta dormir.
Preguntas frecuentes
1) ¿Las transacciones de MongoDB son “ACID reales”?
Dentro de la transacción, sí: atomicidad y aislamiento se proporcionan, y la durabilidad depende del write concern y del journaling.
La trampa en producción es que la topología del clúster y el write concern determinan qué significa “durable” bajo failover.
2) ¿PostgreSQL garantiza no perder datos?
En un solo nodo con ajustes durables y almacenamiento honesto, las transacciones comprometidas sobreviven a crashes.
En un entorno replicado, la política de failover importa: promover una réplica asíncrona puede perder transacciones comprometidas.
3) ¿Cuál es el error más común con transacciones en MongoDB?
Asumir que el write concern por defecto implica commits sobrevivibles. Si necesitas durabilidad segura en failover, sé explícito con w:majority.
4) ¿Cuál es el error más común con transacciones en PostgreSQL?
Mantener transacciones abiertas demasiado tiempo (a menudo “idle in transaction”). Causa bloat, retención de locks y colapso de rendimiento que parece no relacionado—hasta que no lo es.
5) ¿Serializable en PostgreSQL es lo mismo que “ejecución serial”?
Apunta a comportamiento serializable pero usa un enfoque optimista que puede abortar transacciones.
Debes manejar reintentos correctamente, o convertirás corrección en downtime.
6) ¿MongoDB puede dar lecturas linearizables?
MongoDB soporta lecturas linearizables en escenarios limitados, típicamente desde el primario con ajustes específicos.
Son más lentas y más restrictivas; úsalas solo cuando realmente necesites esa garantía.
7) ¿Por qué las transacciones multi-documento de MongoDB son más lentas que operaciones de un documento?
Porque la base de datos debe coordinar múltiples escrituras, rastrear estado de transacción y asegurar semánticas de commit atómico.
En clusters sharded, los costes de coordinación aumentan aún más.
8) ¿Por qué Postgres a veces “se congela” durante cambios de esquema?
Muchas operaciones DDL toman locks pesados que bloquean lecturas/escrituras.
Usa patrones de migración más seguros (índices concurrentes, migraciones por fases y evitar locks largos en horas pico).
9) Si uso MongoDB con w:majority, ¿estoy seguro?
Más seguro, no mágicamente seguro. Aún necesitas lógica de reintentos correcta, idempotencia y un plan para timeouts y resultados desconocidos.
Majority además incrementa la sensibilidad a secondaries lentos y problemas de red.
10) Si uso replicación síncrona en Postgres, ¿estoy seguro?
Reduces el riesgo de perder commits reconocidos, pero intercambias por mayor latencia de commit y dependencia de la salud de réplicas.
Aún necesitas disciplina operacional: monitorización, planificación de capacidad y failover probado.
Conclusión: siguientes pasos prácticos
Deja de tratar las transacciones como una casilla para marcar. Trátalas como un contrato que debes verificar bajo fallo.
PostgreSQL y MongoDB pueden ambos ejecutar sistemas transaccionales fiables, pero hacen promesas diferentes por defecto,
y castigan distintos tipos de pereza.
- Escribe tus requisitos de corrección: qué puede estar obsoleto, qué puede perderse, qué debe ser único, qué debe ser atómico.
- Haz la durabilidad explícita: modo de replicación y política de failover en Postgres; write concern y read concern en MongoDB.
- Practica el fallo: simula stepdowns, mata clientes a mitad de commit y verifica que la aplicación maneje la ambigüedad sin duplicación.
- Instrumenta la verdad: esperas de locks, abortos, lag de replicación, latencia de fsync, elecciones. Si no puedes verlo, no puedes poseerlo.
- Elige la BD que coincida con tus invariantes: si necesitas constraints relacionales y corrección cross-row, predetermina Postgres. Si tu modelo es document-first y mayormente atómico por documento, MongoDB encaja bien—solo no finjas que es Postgres.