La elección de la base de datos que más duele es la que no te das cuenta que tomaste. Entregas una aplicación ordenada, funciona en tu portátil,
funciona en staging, y entonces se encuentra con el tráfico del viernes, un vecino ruidoso o un sistema de archivos ligeramente “creativo”. Ahora estás
aprendiendo sobre locks, fsync y backups por las malas.
PostgreSQL y SQLite son ambos excelentes. También fallan de maneras diferentes. Este es el mapa práctico de dónde se rompe primero cada uno,
por qué, y cómo diagnosticarlo rápido cuando tu pager está haciendo su pequeño baile.
El lente de decisión: ¿qué estás optimizando?
“Confiabilidad frente a simplicidad” suena a debate filosófico hasta que has estado de guardia por ambos.
En la práctica es un intercambio entre dónde vive la complejidad y qué fallo puedes tolerar.
SQLite optimiza para corrección sin fricción… hasta que escalas en el eje equivocado
SQLite es una biblioteca. No hay servidor. Eso no es un detalle bonito; cambia toda tu superficie de fallos.
Obtienes menos piezas móviles, menos puertos, menos demonios, menos credenciales, menos runbooks.
Tu proceso de aplicación es el proceso de la base de datos.
Lo primero que falla no suele ser la “disponibilidad.” Normalmente es la concurrencia de escrituras o suposiciones operativas:
“Podemos poner la BD en NFS,” o “podemos tener 20 workers escribiendo constantemente,” o “no necesitamos backup porque es un archivo.”
PostgreSQL optimiza para comportamiento predecible bajo carga… y exige pagar la tasa del operador
PostgreSQL es un servidor de base de datos. Está diseñado para ser compartido por muchos clientes haciendo muchas cosas a la vez, y es muy bueno en eso.
Pero heredas las labores operativas clásicas: dimensionamiento de memoria, gestión de conexiones, WAL, replicación, vacuum, upgrades, monitorización,
y el ocasional “¿por qué esa consulta de repente es un crimen?”
Lo que falla primero en PostgreSQL rara vez es el archivo de base de datos. Usualmente es latencia (consultas malas, stalls de IO),
deriva operativa (autovacuum mal configurado, almacenamiento cerca del lleno), o error humano (la configuración equivocada en prod).
Mi sesgo: si tu app es de nodo único, volumen de escrituras moderado, y puedes aceptar “escalar o refactorizar más tarde,” SQLite es un regalo.
Si necesitas rendimiento multi-writer, acceso remoto, aislamiento multi-tenant, o alta disponibilidad seria, PostgreSQL es el adulto en la sala.
Aun así puedes hacerte daño con ambos; PostgreSQL simplemente te da más formas de hacerlo con presupuestos más altos.
Hechos interesantes y contexto histórico
- SQLite fue creado en 2000 por D. Richard Hipp para soportar un contrato de la Marina de EE. UU.; fue diseñado para ser pequeño, fiable y autocontenido.
- SQLite es famoso por ser “sin servidor”, lo que significa que no hay un demonio separado; es una biblioteca enlazada a tu proceso, cambiando dominios de fallo y modelos de despliegue.
- SQLite apunta a un formato de archivo estable entre versiones. A menudo puedes mover un archivo de base de datos entre máquinas con mínimo drama—si respetas las reglas del sistema de archivos.
- PostgreSQL desciende de POSTGRES en UC Berkeley (años 80). La parte “SQL” llegó después; la cultura de corrección se quedó.
- PostgreSQL introdujo MVCC temprano como modelo de concurrencia, por eso los lectores no bloquean a los escritores como en sistemas de bloqueo más simples.
- El modo WAL de SQLite (write-ahead logging) se hizo ampliamente usado para mejorar concurrencia y rendimiento separando lecturas de escrituras.
- El WAL de PostgreSQL no es sólo para durabilidad; es la columna vertebral de la replicación y recuperación punto en el tiempo.
- SQLite está en todas partes—teléfonos, navegadores, dispositivos embebidos—porque “es solo un archivo” es exactamente la historia de despliegue que quieren los fabricantes de hardware.
- La línea de “VACUUM” en PostgreSQL es el coste operativo del MVCC: los tuples muertos no desaparecen a menos que el sistema los limpie.
Qué falla primero: PostgreSQL vs SQLite
SQLite: el primer fallo suele ser contención o el sistema de archivos
El dolor de SQLite no es romántico. No es una “caída de la base de datos,” es “¿por qué las solicitudes están atascadas?”, o “¿por qué recibimos ‘database is locked’?”,
o “¿por qué este archivo está corrupto tras un reinicio?”
Fallos comunes “primeros” para SQLite en producción:
- Contención de escritura: demasiados escritores concurrentes, transacciones largas, o un ciclo de checkpoint ocupado.
- Mala ubicación: archivo de base de datos en filesystems de red o almacenamiento inestable; los locks de archivos y garantías de durabilidad se comportan de forma extraña.
- Durabilidad mal configurada: ajustes PRAGMA elegidos por velocidad sin entender el modelo de fallos.
- Backup copiando el archivo en caliente: obtienes un archivo que parece limpio pero es lógicamente inconsistente (o simplemente corrupto).
- Dominio de fallo a nivel de proceso: corrupción de memoria, SIGKILL, o expulsión de contenedor que mata a la app y la BD juntas.
Broma #1: SQLite es como una bicicleta—fiable, elegante y silenciosamente moralista cuando intentas llevar un frigorífico en ella.
PostgreSQL: el primer fallo suele ser deriva operativa o presión de IO
Los fallos iniciales de PostgreSQL son menos sobre corrección y más sobre entropía operativa. Es extremadamente bueno evitando corrupción de datos.
Es menos bueno evitando que tú lo lleves contra una pared con tus propias decisiones.
Fallos comunes “primeros” para PostgreSQL en producción:
- Tormentas de conexiones: demasiadas conexiones cliente, sobrecarga de memoria, cambios de contexto y contención de locks.
- Stalls de IO: almacenamiento lento, WAL mal dimensionado, picos de checkpoint o discos saturados.
- Retraso de autovacuum: bloat, crecimiento de tablas/índices, riesgo de wraparound de transaction ID y degradación de planes de consulta.
- Sorpresas en replicación: lag, slots que retienen WAL indefinidamente, scripts de failover que funcionan hasta que no lo hacen.
- Planes de consulta malos: índices faltantes, estadísticas obsoletas y el ocasional “desactivamos nested loops globalmente porque un blog lo dijo”.
Durabilidad y seguridad ante fallos: qué significa realmente “committed”
La fiabilidad comienza en el límite de commit. Cuando tu código devuelve “OK”, ¿qué acabas de comprar exactamente?
No “probablemente”. No “en caché”. No “quizá cuando el kernel lo decida”. ¿Qué garantizaste realmente?
La durabilidad de SQLite es configurable—y eso es tanto poder como trampa
SQLite te da pragmas que cambian las características de durabilidad. Si no los configuras, heredas valores por defecto que son generalmente seguros,
pero los equipos sensibles al rendimiento a menudo buscan velocidad y accidentalmente venden la seguridad ante fallos.
Palancas clave de SQLite:
- journal_mode: DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF. WAL suele ser el mejor equilibrio práctico.
- synchronous: OFF, NORMAL, FULL, EXTRA. Menor significa más rápido y más arriesgado ante caídas/cortes de energía.
- locking_mode: NORMAL vs EXCLUSIVE; EXCLUSIVE puede mejorar velocidad pero sorprender a otros procesos.
- temp_store: afecta objetos temporales; puede cambiar patrones de IO.
La dura realidad: SQLite puede ser extremadamente seguro ante fallos en un disco local con ajustes correctos. Pero si lo ejecutas en un sistema de archivos que miente
sobre fsync, o en un share de red con locks de asesoría que se comportan distinto, tu historia de durabilidad se vuelve “basada en esperanza”.
La durabilidad de PostgreSQL es más rígida—y más fácil de razonar
PostgreSQL también tiene perillas de durabilidad, pero la cultura y los valores por defecto tienden hacia la corrección. El modelo central:
un commit es durable cuando el registro WAL está persistentemente guardado (sujeto a tu ajuste synchronous_commit).
El WAL de PostgreSQL es una bitácora de grado profesional: se usa para recuperación tras crash, replicación y recuperación punto en el tiempo. Por eso
Postgres puede sobrevivir a un crash y volver con los datos intactos—suponiendo que el almacenamiento respete las semánticas de durabilidad.
La capa de almacenamiento decide quién gana la discusión
Ambas bases de datos dependen del sistema de archivos y el almacenamiento subyacente para honrar escrituras y flushes. Si la plataforma miente, la base de datos pierde.
Caches con respaldo de batería, barreras de escritura y opciones de montaje correctas importan. También importa no usar una combinación filesystem+transport que “funciona la mayor parte del tiempo.”
Una idea parafraseada a menudo atribuida a Werner Vogels (CTO de Amazon): la fiabilidad viene de construir sistemas que asumen fallos y se recuperan rápido en lugar de fingir que no fallarán.
Concurrencia: locks, contención y la forma del dolor
Concurrencia en SQLite: un escritor (mayormente), muchos lectores (usualmente)
SQLite permite múltiples lectores, y con WAL puede mantener lectores mientras un escritor añade al WAL. Pero aún tiene una restricción central:
las transacciones de escritura se serializan. Puedes tener escritores concurrentes en el sentido de múltiples hilos intentando, pero harán cola y fallarán por timeout si
diseñas la app como un sistema OLTP parlanchín.
Lo que esto significa en la práctica:
- Transacciones cortas son supervivencia. Transacciones largas son autolesión.
- Los busy timeouts no son una solución; enmascaran las colas hasta que la latencia es visible para el usuario.
- El comportamiento del checkpoint de WAL puede crear stalls periódicos si no se gestiona (especialmente en discos lentos).
Concurrencia en PostgreSQL: MVCC más locks, que es mejor y más complicado
PostgreSQL brilla bajo concurrencia mixta de lectura/escritura porque los lectores no bloquean a los escritores de forma ingenua. Pero no confundas “MVCC” con “sin locks.”
Postgres tiene muchos locks, además locks pesados, locks ligeros y puntos internos de contención.
La forma típica del dolor en Postgres:
- Una consulta lenta bloquea a otras manteniendo locks más tiempo del esperado.
- Una migración cambia una tabla y provoca colas de locks y timeouts.
- Demasiadas conexiones consumen memoria y saturan CPU por cambios de contexto.
Broma #2: PostgreSQL te dejará hacer casi cualquier cosa—a veces incluyendo cosas que negarás haber hecho durante el postmortem.
Backups y restauraciones: simplicidad frente a garantías
Los backups de SQLite son simples solo si los haces correctamente
SQLite parece amigable para backups porque es un único archivo. Eso seduce a la gente a copiarlo mientras la app escribe.
A veces te sale bien. Luego un día no, y descubres que tu estrategia de backups era “vibes”.
Patrones seguros:
- Usar la API de backup de SQLite (vía
sqlite3 .backupo integración en la aplicación). - Si usas WAL: capturar la base de datos y el estado del WAL de forma coherente (o hacer checkpoint antes de copiar).
- Validar restauraciones regularmente. Un backup que no has restaurado es un rumor.
Los backups de PostgreSQL son más complejos—y mucho más flexibles
Postgres tiene herramientas maduras para backups lógicos (pg_dump), backups físicos (pg_basebackup)
y recuperación punto en el tiempo usando archivado de WAL. La complejidad no es opcional, pero te compra poder operativo real:
restaurar a ayer, clonar producción, recuperar errores humanos y construir réplicas de lectura.
El modo típico de fallo no es “la herramienta de backup no funciona.” Es “nunca probamos restaurar,” o “el archivado de WAL falló silenciosamente,”
o “llenamos el disco con WAL porque un slot de replicación lo dejó retenido.”
Realidad operativa: monitorización, upgrades, migraciones
Operaciones con SQLite: menos perillas, pero tú controlas el ciclo de vida del proceso
SQLite no tiene un servidor que monitorizar, pero aun así necesitas disciplina operativa:
permisos de archivos, espacio en disco, consistencia del filesystem, backups y timeouts a nivel de aplicación.
Tu “upgrade de BD” puede ser una actualización de la biblioteca. Tu “reinicio de BD” es un reinicio de la app.
Si tu app corre como una flota de contenedores sin estado, SQLite se vuelve complicada a menos que cada instancia tenga almacenamiento persistente local y aceptes
bases de datos por instancia o un modelo de sincronización cuidadoso. SQLite es genial siendo local. No es buena pretendiendo ser compartida.
Operaciones con PostgreSQL: obtienes herramientas potentes, y las herramientas potentes cortan
La excelencia operativa en PostgreSQL se parece a la consistencia aburrida:
vigilar el volumen de WAL, lag de replicación, salud del vacuum, consultas lentas, crecimiento de disco y contención de locks.
Los upgrades son manejables, pero son algo a planear. Lo mismo ocurre con migraciones de esquema.
La ventaja es que puedes escalar lecturas con réplicas, escalar escrituras con mejor diseño (o sharding si hace falta), y recuperar de una clase más amplia de desastres.
Tres mini-historias corporativas desde las trincheras
Mini-historia 1: Incidente causado por una suposición equivocada
Un equipo de producto lanzó una herramienta interna pequeña con SQLite porque era “solo metadata”. Corría en una VM compartida, y el archivo de base de datos vivía en un share de red montado.
Esa elección no se debatió; fue implícita. El equipo quería que el archivo sobreviviera a reconstrucciones de VM, y el share era “almacenamiento fiable”.
Semanas después, un evento de mantenimiento provocó un breve fallo de red. La app siguió corriendo. SQLite siguió intentando.
Los logs mostraron errores intermitentes “database disk image is malformed” y “database is locked”. La herramienta no murió completamente; simplemente se volvió poco fiable.
Los usuarios reintentaron. Los reintentos amplificaron las escrituras. El archivo de la base de datos se convirtió en escena del crimen.
El postmortem fue incómodo porque nadie hizo un cambio obvio estúpido. Hicieron una suposición que sonaba razonable:
“Un archivo de base de datos es un archivo, así que cualquier almacenamiento que almacene archivos sirve.” Pero la corrección de SQLite depende de semánticas específicas de locking y durabilidad.
El filesystem de red funcionaba la mayor parte del tiempo—hasta que no.
La solución fue aburrida: mover el archivo SQLite a almacenamiento local, habilitar WAL y crear una pipeline de backups real usando .backup.
Eventualmente, conforme creció el uso, migraron a PostgreSQL para que múltiples servicios pudieran escribir sin tratar un lock de archivo como un algoritmo de consenso distribuido.
Mini-historia 2: Una optimización que salió mal
Otro equipo ejecutaba PostgreSQL para una aplicación orientada al cliente con un patrón OLTP normal: muchas lecturas, escrituras constantes, picos ocasionales.
Un ingeniero nuevo notó picos de latencia durante checkpoints. Leyó algunos posts de tuning y decidió que la solución era “reducir la sobrecarga de flush de disco.”
Cambió parámetros para hacer a Postgres menos ansioso por hacer fsync y ajustó los parámetros de checkpoint agresivamente.
Durante una semana, las gráficas se vieron mejor. La latencia se suavizó. El ingeniero ganó una silenciosa sensación de victoria.
Entonces hubo un reinicio del host durante un corte de energía. Postgres recuperó, pero los últimos minutos de transacciones reconocidas faltaban.
Los usuarios presentaron tickets sobre actualizaciones desaparecidas. El equipo de aplicación inicialmente sospechó de caching. No era caching.
La optimización cambió durabilidad por rendimiento sin una decisión de riesgo documentada. En aislamiento, los ajustes eran “válidos.”
En la realidad, el requisito del negocio era “no perder actualizaciones comprometidas,” y las semánticas del sistema se habían alterado.
La remediación no fue solo revertir los ajustes. Implementaron una regla de gestión de cambios: parámetros que afectan la durabilidad requieren revisión,
y cualquier prueba de rendimiento debe incluir inyección de fallos estilo “quitar la energía” (o lo más cercano que puedas en un laboratorio seguro).
Mini-historia 3: Una práctica aburrida pero correcta que salvó el día
Un grupo de plataforma ejecutaba PostgreSQL con replicación por streaming y una rutina de backups conservadora: backups base nocturnos, archivado continuo de WAL,
y un ejercicio de restauración mensual. No fue trabajo glamoroso. Nadie se promovió por “exitosa restauración #12”.
Una tarde, un ingeniero ejecutó un script de limpieza de datos contra el entorno equivocado. No fue malicioso. Fue un error de memoria muscular:
pestaña del terminal, autocompletado, enter. El script se ejecutó rápido y hizo exactamente lo que le dijeron.
El equipo detectó el problema en minutos vía monitorización: una caída súbita en el conteo de filas y un pico de actividad de delete.
Declararon incidente, congelaron escrituras y eligieron un punto de recuperación justo antes de que el script se ejecutara. Porque el archivado de WAL estaba sano
y los procedimientos de restauración ensayados, realizaron una recuperación punto en el tiempo hacia un nuevo clúster y redirigieron tráfico.
El resultado: una tarde desagradable, pero sin pérdida de datos permanente. Lo que salvó el día no fueron heroísmos. Fueron el hábito de practicar restauraciones
y verificar que los archivos WAL eran realmente utilizables. Las prácticas aburridas están subvaloradas porque no se sienten como ingeniería—hasta que lo son.
Tareas prácticas: comandos, salidas y decisiones
Estos son los tipos de comprobaciones que ejecutas cuando estás decidiendo entre SQLite y Postgres, o cuando algo ya está en llamas.
Cada tarea incluye un comando, una salida de ejemplo, qué significa y qué decisión tomar.
Task 1 (SQLite): Identificar el modo de journaling y nivel de durabilidad
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA journal_mode; PRAGMA synchronous;"
wal
2
Qué significa: El modo WAL está habilitado; synchronous=2 es FULL. Los commits son más duraderos, típicamente más lentos que NORMAL.
Decisión: Para producción en SSD local, WAL + FULL es una base segura. Si la latencia es demasiado alta, prueba NORMAL, pero documenta el riesgo.
Task 2 (SQLite): Comprobar busy timeouts y errores inmediatos de lock
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA busy_timeout;"
5000
Qué significa: El cliente esperará hasta 5 segundos por locks antes de fallar.
Decisión: Si ves latencia visible para el usuario, reduce la contención (transacciones más cortas, menos escritores) en lugar de aumentar esto indefinidamente.
Task 3 (SQLite): Ejecutar una comprobación de integridad tras un crash o evento de almacenamiento
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA integrity_check;"
ok
Qué significa: La estructura de la base de datos es consistente.
Decisión: Si la salida no es ok, detén escrituras, toma una copia para forenses y restaura desde un backup conocido bueno.
Task 4 (SQLite): Inspeccionar estado WAL/checkpoint
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0
Qué significa: El checkpoint tuvo éxito; no quedan frames en WAL; WAL truncado.
Decisión: Si ves valores grandes o fallos, investiga lectores de larga duración o cuellos de IO. Considera programar checkpoints en baja carga.
Task 5 (SQLite): Realizar un backup online seguro
cr0x@server:~$ sqlite3 /var/lib/app/app.db ".backup '/var/backups/app-$(date +%F).db'"
Qué significa: Usa el mecanismo de backup de SQLite, produciendo un snapshot consistente.
Decisión: Prefiere esto sobre cp para bases de datos en vivo. Luego ejecuta PRAGMA integrity_check en el backup como parte de tu pipeline.
Task 6 (Linux): Confirmar que la base de datos no está en un filesystem de red
cr0x@server:~$ df -T /var/lib/app/app.db
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 205113344 73214512 121345024 38% /
Qué significa: ext4 local, no NFS/CIFS. Bueno para las suposiciones de durabilidad de SQLite.
Decisión: Si ves nfs o cifs, reconsidera SQLite para acceso compartido o mueve el archivo a disco local y replica en otro nivel.
Task 7 (PostgreSQL): Comprobar si el servidor está arriba y aceptando conexiones
cr0x@server:~$ pg_isready -h 127.0.0.1 -p 5432
127.0.0.1:5432 - accepting connections
Qué significa: Postgres está arriba y responde a nivel TCP.
Decisión: Si muestra “rejecting” o “no response”, revisa logs, condiciones de disco lleno y estado de recuperación antes de culpar a la app.
Task 8 (PostgreSQL): Identificar presión de conexiones
cr0x@server:~$ psql -X -qAt -c "SELECT count(*) FROM pg_stat_activity;"
187
Qué significa: Existen 187 sesiones backend. Dependiendo del tamaño de la instancia, puede estar bien o ser un problema.
Decisión: Si es alto y está subiendo, implementa un pooler de conexiones y establece límites sensatos en la aplicación.
Task 9 (PostgreSQL): Encontrar locks bloqueantes rápido
cr0x@server:~$ psql -X -qAt -c "SELECT blocked.pid, blocked.query, blocking.pid, blocking.query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE blocked.wait_event_type='Lock';"
4123|UPDATE orders SET status='paid' WHERE id=$1;|3999|ALTER TABLE orders ADD COLUMN note text;
Qué significa: Una migración está bloqueando escrituras de la aplicación.
Decisión: Detén/reviértela si es posible, o reprograma migraciones usando patrones amigables con locks (p. ej., añadir columna sin DEFAULT, backfill por lotes).
Task 10 (PostgreSQL): Medir lag de replicación (si tienes réplicas)
cr0x@server:~$ psql -X -qAt -c "SELECT application_name, state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
replica1|streaming|00:00:00.120|00:00:00.180|00:00:00.450
Qué significa: Lag sub-segundo; sano para muchas cargas.
Decisión: Si el lag son segundos o minutos, investiga saturación de IO, problemas de red o transacciones largas en la réplica.
Task 11 (PostgreSQL): Comprobar si autovacuum se está quedando atrás
cr0x@server:~$ psql -X -qAt -c "SELECT relname, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
events|983421|2025-12-30 08:12:41+00
orders|221904|2025-12-30 08:03:10+00
sessions|110992|2025-12-30 08:15:02+00
Qué significa: Están acumulándose tuples muertos; autovacuum está corriendo pero puede no estar al día con el churn de escrituras.
Decisión: Ajusta autovacuum para tablas calientes, añade índices con cuidado y considera particionar tablas de eventos con alto churn.
Task 12 (PostgreSQL): Identificar consultas con mayor latencia (requiere pg_stat_statements)
cr0x@server:~$ psql -X -qAt -c "SELECT calls, mean_exec_time::numeric(10,2), left(query,80) FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;"
142|812.34|SELECT * FROM reports WHERE org_id = $1 ORDER BY created_at DESC LIMI
9211|203.11|UPDATE sessions SET last_seen = now() WHERE id = $1
Qué significa: Una consulta es consistentemente lenta; otra es moderadamente lenta pero llamada muchas veces.
Decisión: Arregla primero la consulta consistentemente lenta si bloquea flujos de usuario; arregla consultas moderadas de alto llamado si dominan CPU/IO.
Task 13 (OS): Comprobar espacio en disco (porque las bases de datos odian sorpresas)
cr0x@server:~$ df -h /var/lib/postgresql
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 196G 189G 1.9G 99% /
Qué significa: Estás a un archivo de log de un mal día.
Decisión: Libera espacio inmediatamente. Luego implementa alertas al 80/90/95% y planifica crecimiento de capacidad. Disco lleno no es un problema “para después”.
Task 14 (PostgreSQL): Inspeccionar presión de crecimiento de WAL
cr0x@server:~$ psql -X -qAt -c "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_bytes_since_origin;"
1643 GB
Qué significa: Esto es un indicador crudo, pero sugiere generación heavy de WAL a lo largo del tiempo (o que el origen es cero). Mejor: revisa el tamaño del directorio pg_wal y los replication slots.
Decisión: Si el directorio WAL es grande, revisa por replication slots atascados o archivado fallido antes de cambiar knobs de checkpoint.
Task 15 (PostgreSQL): Comprobar replication slots que pueden fijar WAL
cr0x@server:~$ psql -X -qAt -c "SELECT slot_name, active, restart_lsn FROM pg_replication_slots;"
analytics_slot|f|1A/2F000000
Qué significa: Existe un slot inactivo. Puede retener WAL hasta que un consumidor se ponga al día o el slot sea eliminado.
Decisión: Si el consumidor se fue, elimina el slot tras confirmar que es seguro. Si es necesario, arregla el consumidor y planifica retención de WAL.
Guía de diagnóstico rápido
Cuando la latencia sube o las escrituras fallan, no tienes tiempo para un debate filosófico. Necesitas un camino corto hacia “¿dónde está el cuello de botella?”
Esta guía es intencionalmente opinada.
Primero: identifica en qué clase de fallo estás
- Caída dura: no se puede conectar / el archivo no se abre / errores de corrupción.
- Caída suave: las conexiones funcionan pero las solicitudes hacen timeout.
- Resultados erróneos: datos faltantes, lecturas inconsistentes o actualizaciones parciales.
Segundo: revisa el sustrato (disco y filesystem) antes de culpar al SQL
- ¿El disco está casi lleno?
- ¿Está el IO saturado o lento?
- ¿La base de datos está en un filesystem de red (peligro para SQLite) o en un volumen inestable?
Tercero: revisa señales de contención
- SQLite: “database is locked,” transacciones largas, stalls de checkpoint WAL, busy timeouts.
- PostgreSQL: esperas de locks, conteos de conexiones, consultas lentas, retraso de autovacuum, lag de replicación.
Cuarto: decide la mitigación segura más rápida
- SQLite: reduce concurrencia de escritores, acorta transacciones, habilita WAL, añade busy_timeout (como parche), mueve a disco local.
- Postgres: mata la consulta bloqueante, pausa una migración, añade un índice con cuidado, escala IO, habilita pooling o falla sobre si el primario está enfermo.
Quinto: escribe el modo de fallo en una frase
Si no puedes describirlo, no puedes arreglarlo de forma confiable. Ejemplos:
“Escrituras hacen cola detrás de una transacción larga que mantiene un lock” o “Archivo SQLite en NFS perdió semánticas de lock durante un fallo de red.”
Errores comunes: síntoma → causa raíz → solución
Errores de SQLite
-
Síntoma: frecuentes “database is locked” bajo carga
Causa raíz: demasiados escritores concurrentes o transacciones de escritura largas; el modo journal rollback por defecto amplifica la contención
Solución: habilitar WAL, reducir concurrencia de escritura, mantener transacciones cortas, añadir batching dirigido, establecer un busy_timeout sensato. -
Síntoma: corrupción de la base de datos tras reinicio/corte de energía
Causa raíz: synchronous=OFF/NORMAL elegido sin entender; almacenamiento que miente sobre fsync; o BD colocada en filesystem inadecuado
Solución: usar disco local, establecer synchronous=FULL para datos críticos, evitar filesystems de red, validar con integrity_check y ejercicios de restauración. -
Síntoma: el backup se restaura pero la app se comporta raro (filas faltantes, errores de constraints después)
Causa raíz: copia del archivo en caliente durante escrituras activas; WAL no capturado de forma coherente
Solución: usar la API de backup de SQLite; checkpointar apropiadamente; probar restauración y ejecutar integrity_check en backups. -
Síntoma: picos de latencia periódicos cada pocos minutos
Causa raíz: checkpointing WAL causando ráfagas de IO, a menudo empeorado por discos lentos o lectores largos que impiden completar checkpoints
Solución: ajustar la estrategia de checkpoints, reducir transacciones de lectura largas, considerar checkpoint manual en baja carga.
Errores de PostgreSQL
-
Síntoma: CPU alta y errores de “too many connections”
Causa raíz: patrón one-connection-per-request; falta de pooling; max_connections elevado hasta que la memoria sufre
Solución: usar un pooler de conexiones, limitar conexiones, mejorar el reuso de conexiones en la app, monitorizar pg_stat_activity. -
Síntoma: picos de latencia de escritura y periodos intensivos de fsync
Causa raíz: picos de checkpoint, WAL en almacenamiento lento, shared_buffers/checkpoint mal dimensionados, o IO saturado
Solución: poner WAL en almacenamiento rápido, ajustar checkpoint_timeout y checkpoint_completion_target con cuidado, medir IO con herramientas del sistema. -
Síntoma: consultas que se enlentecen con el tiempo, índices crecen, uso de disco se dispara
Causa raíz: vacuum quedándose atrás; acumulación de bloat; transacciones largas que impiden limpieza
Solución: afinar autovacuum por tabla, eliminar transacciones largas, considerar particionado, ejecutar vacuum/analyze según convenga. -
Síntoma: disco se llena con WAL inesperadamente
Causa raíz: slot de replicación fijando WAL; réplica offline; archivado de WAL roto y retención crece
Solución: inspeccionar replication slots, restaurar consumidores, eliminar slots no usados, alertar sobre tamaño de pg_wal y errores del archiver. -
Síntoma: una migración causa timeouts generalizados
Causa raíz: DDL adquiriendo locks; transacciones largas bloquean DDL y viceversa
Solución: usar patrones de migración que minimicen locks, establecer lock timeouts, desplegar en ventanas de baja carga, verificar bloqueos con pg_blocking_pids.
Checklists / plan paso a paso
Checklist A: Cuando SQLite es la elección correcta (y cómo no arrepentirte)
- Confirma la forma de la carga: mayormente lecturas, escrituras concurrentes limitadas, transacciones cortas.
- Coloca la BD en disco local: evita NFS/CIFS y “volúmenes compartidos” con semánticas de locking poco claras.
- Configura modo WAL: usa WAL para mejor concurrencia lectura/escritura.
- Elige durabilidad intencionalmente: synchronous=FULL para escrituras críticas; documenta si eliges NORMAL.
- Implementa backups usando la API de backup: programa, rota y verifica restauraciones.
- Añade chequeos de integridad: ejecuta integrity_check en CI para artefactos de backup o tras cierres no limpios.
- Planifica tu salida: define el umbral en el que migras a Postgres (escritores, acceso remoto, multi-instancia).
Checklist B: Cuando PostgreSQL es la elección correcta (y cómo mantenerlo aburrido)
- Tamaño correcto de conexiones: no equipares “más conexiones” con “más throughput.” Usa pooling.
- Pon el WAL en buen almacenamiento: discos de baja latencia importan más de lo que quieres admitir.
- Habilita visibilidad esencial: logging de consultas lentas, pg_stat_statements, monitor de locks, monitor de replicación.
- Haz del vacuum una prioridad: vigila bloat, tuples muertos y transacciones largas.
- Backups + ejercicios de restauración: elige lógico/físico/PITR según RPO/RTO y practica restauraciones.
- Los cambios de esquema son despliegues: practica migraciones seguras y establece lock timeouts.
- Tener un plan de failover: aunque sea manual, escríbelo y pruébalo cuando estés en calma.
Paso a paso: elegir entre ellos en un proyecto real
- Escribe la tolerancia a fallos: pérdida de datos aceptable (RPO) y tiempo de inactividad aceptable (RTO).
- Cuantifica la concurrencia: número de escritores concurrentes y tiempo de transacción más largo en la ruta crítica.
- Decide la topología de despliegue: nodo único vs multi-instancia; necesidad de acceso remoto; necesidad de réplicas.
- Elige lo más simple que cumpla el SLO: SQLite si nodo único y baja contención de escrituras; Postgres en caso contrario.
- Prototipa el peor caso: prueba carga de escrituras; inyecta fallos (kill -9, reinicio en staging, simulación de latencia de disco).
- Operacionaliza: backups, alertas, dashboards y pruebas de restauración antes de declararlo “hecho”.
Preguntas frecuentes
1) ¿SQLite es “menos fiable” que PostgreSQL?
No inherentemente. SQLite puede ser extremadamente fiable en almacenamiento local con ajustes sensatos. Es menos tolerante cuando añades concurrencia,
filesystems compartidos o prácticas de backup descuidadas. PostgreSQL está construido para acceso compartido y patrones de recuperación operativa, por eso tiende a mantenerse fiable conforme la complejidad crece.
2) ¿Cuál es la forma más común en que SQLite falla en producción?
Contención de locks: demasiados escritores o transacciones muy largas. El síntoma es “database is locked,” timeouts o latencia visible al usuario.
La solución es arquitectónica (reducir concurrencia de escrituras) más que magia con PRAGMA.
3) ¿Puedo ejecutar SQLite en NFS o un volumen compartido de Kubernetes?
Puedes, pero apuestas tus datos a semánticas de filesystem que probablemente no has probado bajo fallo. Si necesitas acceso compartido entre nodos,
PostgreSQL es el predeterminado más seguro. Si debes usar SQLite, mantenlo en almacenamiento persistente local del nodo y trátalo como estado por instancia.
4) ¿El modo WAL hace a SQLite “multi-writer”?
No. WAL mejora la concurrencia lectura/escritura y reduce que escritores bloqueen lectores, pero las escrituras todavía se serializan. Puedes reducir el dolor, no cambiar el modelo central.
5) ¿Cuál es la forma más común en que PostgreSQL falla primero?
Sobrecarga operativa: demasiadas conexiones, saturación de IO o vacuum quedándose atrás. Postgres usualmente mantiene la corrección; simplemente se vuelve lento o queda atascado por locks.
6) Si PostgreSQL es más potente, ¿por qué no usarlo siempre?
Porque la tasa del operador es real. Necesitas backups, monitorización, upgrades y planificación de capacidad. Para apps pequeñas de nodo único,
SQLite puede ofrecer excelente fiabilidad con mucha menos superficie operativa.
7) ¿Cómo difieren los backups en el sentido de “qué falla primero”?
El riesgo de SQLite es “copiamos mal el archivo.” El riesgo de PostgreSQL es “configuramos backups pero nunca probamos restaurar,” o “el archivado de WAL falló silenciosamente.”
Ambos son problemas humanos; Postgres simplemente te da más formas de hacerlo bien si te comprometes a la práctica.
8) ¿Cuál es más fácil de depurar durante un incidente?
PostgreSQL, por lo general. Tienes visibilidad: pg_stat_activity, locks, estadísticas de consultas, vistas de replicación. La depuración de SQLite a menudo comienza en los logs de la aplicación y en el comportamiento de archivos/IO a nivel OS.
La simplicidad de SQLite reduce la superficie de fallo, pero cuando falla, la causa suele estar fuera del “SQL.”
9) ¿Qué hay del riesgo de corrupción de datos?
Ambos están diseñados para evitar corrupción. El riesgo mayor es el entorno: almacenamiento no fiable, ajustes inseguros y procedimientos de backup incorrectos.
SQLite está más expuesto a rarezas del filesystem porque es un único archivo con expectativas de locking. PostgreSQL está más expuesto a mala configuración operativa y eventos de disco lleno.
10) ¿Cuál es la ruta de migración limpia si empiezo con SQLite?
Planifícalo temprano: mantén esquemas compatibles cuando sea posible, evita rarezas exclusivas de SQLite y construye una herramienta de migración que pueda exportar/importar de forma determinista.
Cuando llegue el momento, haz dual-write o un corte controlado con verificación, no un “solo SQL” de viernes por la noche.
Siguientes pasos que puedes hacer esta semana
Si estás eligiendo hoy: elige SQLite para apps de nodo único con concurrencia de escrituras modesta y fuerte preferencia por operaciones mínimas.
Elige PostgreSQL cuando necesites concurrencia, acceso remoto, HA o cuando no puedas tolerar “funcionó hasta que cambió la carga.”
Luego haz el trabajo poco sexy:
- Para SQLite: configura WAL + nivel de synchronous intencional, mueve la BD a almacenamiento local y haz backup con la API de backup. Prueba restaurar.
- Para PostgreSQL: limita conexiones y añade pooling, vigila locks y salud del vacuum, e implementa backups con ejercicios de restauración. Trata las migraciones como cambios de producción.
- Para ambos: alerta sobre espacio en disco, valida suposiciones del filesystem y escribe tu modelo de fallo en lenguaje llano.
La base de datos ganadora es la cuyas fallas has ensayado. La perdedora es la que elegiste porque se sentía simple, hasta que dejó de serlo.