Estás en un VPS. Quieres “una base de datos”. No un proyecto de fin de semana, ni un rebaño de yaks. Algo que no te despierte a las 03:00 porque un único archivo se quedó atascado, o porque tu app de pronto tiene tráfico real y tu elección “simple” se convierte en una migración con dientes.
La forma más rápida de elegir entre PostgreSQL y SQLite es dejar de discutir características y empezar a hacer una pregunta brutal: ¿dónde está tu límite de concurrencia y de fallo? Si está dentro de un solo proceso, SQLite es un bisturí. Si está entre muchos procesos, usuarios, trabajos y conexiones, PostgreSQL es la llave aburrida y probada en batalla.
La decisión en un minuto
Si solo lees esta sección, aún tomarás una decisión respetable.
Elige SQLite si todas estas son ciertas
- Tu app es mayormente de un solo escritor y de tráfico moderado (piensa: un proceso web o un worker de cola que hace escrituras, no un enjambre).
- Puedes convivir con semánticas de bloqueo por fichero y algún “database is locked” ocasional si lo usas mal.
- Quieres cero sobrecarga operativa: sin demonio, sin ajuste de vacuum en segundo plano, sin drama de pooling de conexiones.
- Tu dominio de fallo es “este VPS y este disco” y estás bien con eso.
- Quieres paridad de desarrollo local fácil: distribuir un solo archivo de BD es una ventaja potente.
Elige PostgreSQL si alguna de estas es cierta
- Tienes múltiples escritores, varias instancias de la app, cron jobs, workers, consultas analíticas, herramientas de administración… cualquier cosa que se comporte como una pequeña multitud.
- Necesitas concurrencia robusta sin convertir tu app en un coordinador de bloqueos.
- Te importan las garantías de aislamiento, durabilidad y recuperabilidad frente a modos de fallo del mundo real.
- Quieres cambios de esquema en línea, indexación más rica y planes de consulta que escalen más allá de “mono lindo”.
- Prevés crecimiento y prefieres escalar añadiendo CPU/RAM ahora y réplicas después, en lugar de hacer una migración de alto riesgo más adelante.
Regla empírica: si tu base de datos tiene que mediar la impaciencia humana (tráfico web) y la impaciencia de la máquina (trabajos), PostgreSQL es el adulto en la sala.
Broma #1: SQLite es como una bicicleta: rápida, elegante y perfecta hasta que intentas mover un sofá con ella.
Un modelo mental que evita arrepentimientos
La mayoría de los debates “Postgres vs SQLite” mueren porque la gente compara sintaxis SQL o listas de características. La elección es realmente sobre la forma operativa: quién habla con la base de datos, con qué frecuencia y qué pasa cuando las cosas van mal.
SQLite: una biblioteca con un archivo, no un servidor
SQLite se ejecuta en proceso. No hay un demonio servidor aceptando conexiones. Tu app enlaza una biblioteca; la “base de datos” es un archivo (más archivos opcionales de journaling/WAL). Eso significa:
- La latencia puede ser excelente porque no hay salto de red. Las llamadas son llamadas a funciones.
- La concurrencia está limitada por el bloqueo de archivos. Las lecturas van bien. Las escrituras requieren coordinación; WAL mejora esto pero no lo convierte en una orgía de escrituras.
- La durabilidad depende de las semánticas del sistema de archivos, opciones de montaje y tu uso de ajustes síncronos. No es “inseguro”, es “tú manejas los bordes afilados”.
- Las copias de seguridad son copias de archivos, lo cual puede ser maravillosamente simple—hasta que haces una copia en el momento equivocado sin usar las API de backup de SQLite.
PostgreSQL: un servidor con procesos, memoria y opiniones
PostgreSQL se ejecuta como servidor de base de datos con sus propios procesos, caches, write-ahead log (WAL), vacuum en segundo plano y semánticas transaccionales bien definidas. Eso implica:
- Alta concurrencia con MVCC (control de concurrencia multiversión): los lectores no bloquean a los escritores como esperarías con bloqueos de archivo.
- Durabilidad y recuperación tras fallos son pilares. Aún necesitas configurar y probar, pero el sistema está diseñado para los días malos.
- Existe sobrecarga operativa: actualizaciones, backups, monitorización, vacuum y gestión de conexiones.
- Las rutas de escalado son más claras: replicación, réplicas de lectura, particionado, poolers de conexión y herramientas maduras.
La pregunta del límite
Pregunta: “¿Es la base de datos un límite de servicio compartido?” Si la respuesta es sí, PostgreSQL. Si no, SQLite puede ser una base de datos de producción legítima. No subestimes cuán a menudo “no” se transforma silenciosamente en “sí” cuando añades un worker, luego una segunda instancia de la app, luego un panel de administración que ejecuta consultas pesadas.
Datos interesantes y un poco de historia
Un poco de contexto ayuda porque las decisiones de diseño no fueron arbitrarias. Son cicatrices de uso real.
- SQLite nació en 2000 como una base de datos embebida para evitar la sobrecarga de DB cliente/servidor para un proyecto específico; se convirtió en el motor SQL “pequeño” por defecto del mundo.
- PostgreSQL se remonta a los años 80 (proyecto POSTGRES en UC Berkeley), y su ADN se nota: extensibilidad, corrección y una obsesión académica por el comportamiento transaccional.
- SQLite es posiblemente el motor más desplegado porque se incluye en teléfonos, navegadores, sistemas operativos y multitud de aplicaciones como biblioteca.
- PostgreSQL popularizó la extensibilidad rica mediante tipos personalizados, operadores y extensiones; por eso es la plataforma “SQL plus” por defecto en muchas pilas modernas.
- El modo WAL de SQLite se añadió más tarde para reducir el bloqueo de escritores y mejorar la concurrencia; cambió para qué casos “SQLite es bueno” en producción.
- El MVCC de PostgreSQL hace que versiones antiguas de filas permanezcan hasta que vacuum las limpia; esto es una característica de rendimiento y una tarea operativa.
- SQLite es famoso por la portabilidad estricta de archivos de base de datos entre arquitecturas y versiones, pero aún depende del comportamiento del sistema de archivos para la durabilidad.
- El WAL de PostgreSQL también se llama WAL (mismo acrónimo, implementaciones distintas), y es la base para replicación y recuperación punto-en-tiempo.
- “database is locked” en SQLite no es un bug; es una consecuencia explícita del modelo de bloqueo. El bug es asumir que se comporta como una BD servidor.
Realidades del VPS: discos, memoria y vecinos
Un VPS no es un portátil ni una base de datos gestionada. Es una pequeña porción de una máquina más grande con IO compartido y, a veces, vecinos impredecibles. Tu elección de base de datos debería respetar eso.
El IO de disco es la primera mentira que dicen tus benchmarks
En un VPS, tu “SSD” puede ser rápido, o puede ser “rápido cuando los vecinos duermen”. SQLite y PostgreSQL ambos se preocupan por el comportamiento de fsync, pero lo experimentan de forma distinta:
- SQLite escribe en un único archivo de base de datos (más journaling/WAL). Las escrituras aleatorias pueden ser castigadoras si tu carga tiene mucha churn.
- PostgreSQL escribe en múltiples archivos: archivos de datos y segmentos WAL. Las escrituras WAL son más secuenciales y pueden ser más amables con discos reales, pero ahora tienes procesos en segundo plano y checkpoints.
La memoria no es solo “cache”; es política
SQLite depende en gran medida del caché de páginas del SO. Eso está bien—Linux es bueno para cachear. PostgreSQL tiene sus propios shared buffers además del caché del SO. Si lo dimensionas mal en un VPS pequeño, puedes terminar con doble cache y dejando al resto del sistema sin recursos.
El modelo de procesos importa cuando tienes poca RAM
SQLite vive dentro del proceso de tu app. PostgreSQL usa múltiples procesos y memoria por conexión. En un VPS de 1 GB, un montón de conexiones inactivas puede ser un fallo de rendimiento, no un detalle menor. Si ejecutas Postgres en hierro pequeño, aprenderás a amar el pooling de conexiones.
Radio de impacto operativo
El radio de impacto de SQLite suele ser “este archivo”. El de PostgreSQL es “este clúster”, pero con mejores herramientas para aislar y recuperar. SQLite puede recuperarse copiando un archivo—a menos que lo copies en el momento equivocado. PostgreSQL puede recuperarse reproduciendo WAL—a menos que nunca hayas probado tus backups. Elige tu veneno; luego mitígalo.
Tareas prácticas: comandos, salidas, decisiones (12+)
Abajo hay tareas que puedes ejecutar en un VPS hoy. Cada una te da una señal, no una corazonada. El objetivo es decidir basándote en evidencia: capacidad de IO, necesidades de concurrencia y riesgos de fallo.
Task 1: Comprueba la presión de CPU y memoria (¿te permiten siquiera ejecutar Postgres?)
cr0x@server:~$ lscpu | egrep 'Model name|CPU\(s\)'
CPU(s): 2
Model name: Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 1.0Gi 220Mi 180Mi 12Mi 620Mi 690Mi
Swap: 1.0Gi 0B 1.0Gi
Qué significa: Con 1 GB de RAM, Postgres es posible pero debes ser disciplinado (pool de conexiones, ajustar memoria). SQLite se sentirá sin esfuerzo.
Decisión: Si no puedes permitirte unos cientos de MB para Postgres más margen para tu app, prefiere SQLite o mejora el VPS.
Task 2: Identifica el tipo de almacenamiento y opciones de montaje (la durabilidad vive aquí)
cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /
/dev/vda1 ext4 rw,relatime,errors=remount-ro
Qué significa: ext4 con relatime es normal. Si ves opciones extrañas como data=writeback o sistemas de archivos en red exóticos, debes tratar las afirmaciones de durabilidad de SQLite con sospecha y ajustar Postgres también con cuidado.
Decisión: Si estás en almacenamiento en red o extraño, Postgres con WAL+fsync probado suele ser más seguro que “copiar el archivo de la BD”.
Task 3: Chequeo rápido de latencia de disco (tu futuro ticket “bd lenta”)
cr0x@server:~$ iostat -xz 1 3
Linux 6.2.0 (server) 12/30/2025 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
3.10 0.00 1.20 0.40 0.10 95.20
Device r/s w/s rkB/s wkB/s await svctm %util
vda 5.00 8.00 80.0 210.0 2.10 0.40 0.52
Qué significa: await en dígitos bajos es aceptable. Si ves picos de 20–100 ms, tanto SQLite como Postgres sufrirán, pero SQLite lo mostrará como bloqueos dentro de los hilos de la app.
Decisión: Alto IO wait aboga por Postgres con ajuste cuidadoso de checkpoints y posiblemente mover a mejor almacenamiento; también aboga por reducir la amplificación de escritura de cualquier forma.
Task 4: Mide el coste de sync del sistema de archivos (SQLite y Postgres pagan esta factura)
cr0x@server:~$ sudo dd if=/dev/zero of=/var/tmp/fsync.test bs=4k count=25000 conv=fdatasync status=progress
102400000 bytes (102 MB, 98 MiB) copied, 1.52 s, 67.4 MB/s
25000+0 records in
25000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 1.52 s, 67.3 MB/s
Qué significa: Es burdo, pero aproxima “qué doloroso es forzar durabilidad”. Si esto es glacial, tus ajustes “seguros” harán daño.
Decisión: Si el sync forzado es caro, SQLite necesita WAL + ajustes síncronos sensatos; Postgres necesita ajuste de checkpoints y no abusar de synchronous_commit para escrituras no críticas.
Task 5: Verifica límites de archivos abiertos (a Postgres le importará más)
cr0x@server:~$ ulimit -n
1024
Qué significa: 1024 es limitado para Postgres bajo carga con muchas conexiones y archivos. SQLite se preocupa menos, pero tu app podría.
Decisión: Si eliges Postgres, aumenta los límites vía systemd o limits.conf; si no puedes, mantén conexiones bajas y usa un pooler.
Task 6: Inspecciona el recuento de conexiones en vivo (si ya es una multitud, SQLite se pondrá picante)
cr0x@server:~$ sudo ss -tanp | awk '$4 ~ /:5432$/ {c++} END {print c+0}'
0
Qué significa: No hay Postgres ahora, pero el patrón es lo que importa: ¿cuántos clientes DB concurrentes existirán?
Decisión: Si esperas docenas/cientos de conexiones concurrentes, Postgres más un pooler gana. SQLite no tiene “conexiones” en el mismo sentido; tiene “hilos y procesos peleando por un archivo”.
Task 7: Crea una base SQLite con WAL e inspecciona pragmas (hazla menos frágil)
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA wal_autocheckpoint=1000;'
wal
Qué significa: Modo WAL activado; synchronous NORMAL es un compromiso común (suficientemente duradero para muchas apps, menos dolor IO que FULL).
Decisión: Si eliges SQLite, debes ser explícito con los pragmas. Los ajustes por defecto no son “política de producción”, son “valores genéricos de biblioteca”.
Task 8: Simula escrituras concurrentes en SQLite (detecta el muro de bloqueo temprano)
cr0x@server:~$ for i in $(seq 1 20); do (sqlite3 /var/lib/myapp/app.db "BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS t(x); INSERT INTO t VALUES($i); COMMIT;" >/dev/null 2>&1 &); done; wait; echo done
done
Qué significa: Es una prueba gruesa. Si la repites con más contención y empiezas a ver “database is locked” en stderr, esa es tu sirena de advertencia.
Decisión: Si tu carga real se parece a esto (muchos escritores), deja de romantizar SQLite y usa Postgres.
Task 9: Instala Postgres y confirma la salud del servicio
cr0x@server:~$ sudo apt-get update -qq
...output...
cr0x@server:~$ sudo apt-get install -y postgresql
...output...
cr0x@server:~$ sudo systemctl status postgresql --no-pager
● postgresql.service - PostgreSQL RDBMS
Loaded: loaded (/lib/systemd/system/postgresql.service; enabled)
Active: active (exited)
Qué significa: En Debian/Ubuntu, el servicio wrapper puede mostrar “active (exited)” mientras las unidades del clúster se ejecutan. No te asustes; revisa el clúster.
Decisión: Si no puedes mantener un servicio saludable en tu VPS (permisos, disco lleno, presión de memoria), SQLite podría ser la opción más sensata hasta estabilizar el host.
Task 10: Comprueba la preparación del clúster Postgres
cr0x@server:~$ pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
16 main 5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log
Qué significa: Está online. Tienes un directorio de datos y una ruta de log—dos cosas que aprenderás a respetar.
Decisión: Si Postgres arranca limpio y se mantiene online con tu app, es una señal fuerte de que puedes permitirte la operación.
Task 11: Inspecciona ajustes de durabilidad y checkpoints de Postgres (no vayas a ciegas)
cr0x@server:~$ sudo -u postgres psql -c "SHOW synchronous_commit; SHOW fsync; SHOW full_page_writes; SHOW checkpoint_timeout; SHOW max_wal_size;"
synchronous_commit
-------------------
on
(1 row)
fsync
-------
on
(1 row)
full_page_writes
------------------
on
(1 row)
checkpoint_timeout
--------------------
5min
(1 row)
max_wal_size
--------------
1GB
(1 row)
Qué significa: Los valores por defecto son conservadores. Apuntan a seguridad en hardware genérico, no necesariamente a tu VPS específico.
Decisión: Si necesitas alto rendimiento de escritura, puedes ajustar checkpoints y tamaño WAL. Si necesitas máxima seguridad, mantén estos valores conservadores e invierte en backups y pruebas.
Task 12: Detecta presión de vacuum (el “impuesto de mantenimiento” de Postgres)
cr0x@server:~$ sudo -u postgres psql -c "SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_dead_tup
---------+------------
(0 rows)
Qué significa: Todavía no hay tablas de usuario. Más adelante, esto muestra si los tuples muertos se están acumulando. Los montones significan bloat, consultas más lentas y, eventualmente, miseria por paging.
Decisión: Si eliges Postgres, debes monitorizar vacuum/bloat. Si no puedes comprometerte con eso, la simplicidad de SQLite empieza a verse atractiva—siempre que la concurrencia cuadre.
Task 13: Identifica consultas lentas en Postgres (decide si necesitas índices u otra BD)
cr0x@server:~$ sudo -u postgres psql -c "SHOW shared_preload_libraries;"
shared_preload_libraries
--------------------------
(1 row)
Qué significa: Si pg_stat_statements no está habilitado, te falta una lente clave para el comportamiento de consultas.
Decisión: Si ejecutas algo más que tráfico de juguete, habilita estadísticas de consultas y trátalas como telemetría de producción. SQLite tiene opciones, pero Postgres hace este tipo de análisis rutinario.
Task 14: Estima tamaño y crecimiento de la BD (archivo SQLite vs clúster Postgres)
cr0x@server:~$ du -sh /var/lib/myapp/app.db
48M /var/lib/myapp/app.db
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size(current_database()));"
pg_size_pretty
----------------
7289 kB
(1 row)
Qué significa: SQLite es un archivo; Postgres es un árbol de directorios más WAL. Los patrones de crecimiento difieren: el archivo SQLite crece y puede no reducirse; Postgres puede inflarse a menos que se vacuumee.
Decisión: Si necesitas gestión de tamaño predecible y retención, Postgres con vacuum y particionado adecuados (si se requiere) tiende a ser más fácil de controlar que un archivo monolítico.
Task 15: Prueba el flujo de backup/restore (esto decide si duermes)
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db ".backup '/var/backups/app.db.bak'"
cr0x@server:~$ ls -lh /var/backups/app.db.bak
-rw-r--r-- 1 root root 48M Dec 30 03:12 /var/backups/app.db.bak
cr0x@server:~$ sudo -u postgres pg_dump -Fc -f /var/backups/pg.dump postgres
cr0x@server:~$ ls -lh /var/backups/pg.dump
-rw-r--r-- 1 postgres postgres 36K Dec 30 03:13 /var/backups/pg.dump
Qué significa: Ambos pueden ser respaldados. La clave es la consistencia y probar la restauración. SQLite necesita el método de backup correcto; Postgres necesita que practiques restaurar y permisos.
Decisión: Si no puedes o no vas a probar restauraciones, no elijas ninguno—porque no estás eligiendo una base de datos, estás eligiendo un incidente futuro.
Guía rápida de diagnóstico
Esta es la secuencia de triage para “algo está lento”. El objetivo es aislar el cuello de botella en minutos, no debatir arquitectura en Slack durante horas.
Primero: ¿es CPU, memoria o disco?
cr0x@server:~$ uptime
03:20:11 up 12 days, 2:41, 1 user, load average: 0.22, 0.40, 0.35
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 184320 28000 635000 0 0 10 25 120 180 3 1 95 1 0
0 0 0 183900 28000 635200 0 0 0 0 110 170 2 1 97 0 0
Interpretación: Alto wa significa espera de IO de disco; alto si/so significa swap; alto r con poco idle significa presión de CPU.
Acción: Si el host está intercambiando, arregla la memoria primero (reduce conexiones, ajusta Postgres, añade RAM). Si la espera IO es alta, mira checkpointing, costes de fsync y patrones de escritura.
Segundo: ¿la base de datos está bloqueada o en espera?
SQLite: busca errores de bloqueo en logs de la app; comprueba si haces transacciones largas.
Postgres: comprueba bloqueos que estén bloqueando.
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, wait_event_type, wait_event, state, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY pid;"
pid | wait_event_type | wait_event | state | query
------+-----------------+------------+--------+-------
(0 rows)
Interpretación: Si ves sesiones esperando por bloqueos, no estás “lento”, estás serializado. Arreglo distinto: acorta transacciones, añade índices para reducir duración de bloqueos, evita DDL de larga ejecución en horas pico.
Tercero: ¿es un problema de consulta o de capacidad?
Para Postgres, identifica consultas lentas y haz EXPLAIN. Para SQLite, examina patrones de acceso e índices y considera mover consultas pesadas fuera del camino caliente.
cr0x@server:~$ sudo -u postgres psql -c "EXPLAIN (ANALYZE, BUFFERS) SELECT 1;"
QUERY PLAN
--------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.003..0.004 rows=1 loops=1)
Planning Time: 0.020 ms
Execution Time: 0.010 ms
(3 rows)
Interpretación: En uso real, busca scans secuenciales en tablas grandes, grandes hits de buffers o tiempo gastado esperando IO.
Acción: Si las consultas son lentas por falta de índices, arregla el esquema. Si son lentas porque el disco es lento, mejora el almacenamiento o reduce churn de escritura. Si son lentas por concurrencia, arregla el pooling de conexiones o elige la BD correcta.
Errores comunes (síntomas → causa raíz → solución)
Estos no son fracasos morales. Son resultados predecibles de tratar la base de datos como una caja negra.
1) “database is locked” aparece esporádicamente (SQLite)
Síntomas: Errores de la app bajo carga, picos durante jobs en segundo plano, peticiones fallando y luego teniendo éxito al reintentar.
Causa raíz: Múltiples escritores o transacciones largas sosteniendo bloqueos de escritura. WAL ayuda, pero un único escritor aún necesita tiempo.
Solución: Habilita WAL; mantén transacciones cortas; serializa escrituras vía una cola de trabajos; añade busy_timeout; o migra a Postgres si necesitas escrituras concurrentes.
2) SQLite se siente rápido hasta que despliegas múltiples instancias de la app
Síntomas: Funciona en dev, inestable en prod; el rendimiento cae solo después de escalar horizontalmente.
Causa raíz: El bloqueo de archivos entre procesos se vuelve contención. Además: los sistemas de archivos compartidos son una trampa.
Solución: No compartas SQLite sobre NFS. Si necesitas más de un proceso escritor, usa Postgres.
3) Postgres está “lento” pero la CPU está inactiva
Síntomas: Alta latencia, CPU baja, paradas periódicas.
Causa raíz: Espera de IO durante checkpoints o carga de escritura pesada con muchos fsync; max_wal_size muy pequeño; almacenamiento pobre.
Solución: Aumenta max_wal_size; ajusta checkpoints; mueve WAL a disco más rápido si es posible; reduce escrituras síncronas para rutas no críticas (con cuidado).
4) Postgres se cae con muchas conexiones en un VPS pequeño
Síntomas: Picos de memoria, kills por OOM, “too many clients”, timeouts aleatorios.
Causa raíz: Patrón de una conexión por petición; overhead por conexión; sin pooling.
Solución: Usa PgBouncer; reduce max_connections; usa un tamaño de pool sensato; cambia la app para reutilizar conexiones.
5) Existen backups pero las restauraciones fallan
Síntomas: Prueba de restore falla; permisos rotos; roles faltantes; archivo de backup de SQLite corrupto o inconsistente.
Causa raíz: Backups tomados incorrectamente (copia de archivo SQLite durante escritura) o no probados (pg_dump sin globals/roles).
Solución: Para SQLite, usa .backup o la API de backup; para Postgres, realiza simulacros de restauración incluyendo roles y esquema; automatiza la verificación.
6) Tablas de Postgres se inflan y las consultas degradan con semanas
Síntomas: Uso de disco crece más rápido que los datos; índices hinchados; consultas lentas; vacuum ejecutándose constantemente.
Causa raíz: Tuplas muertas de MVCC se acumulan; autovacuum no da abasto; patrones agresivos de UPDATE/DELETE.
Solución: Ajusta autovacuum por tabla; evita hot updates donde sea posible; considera particionado o mantenimiento periódico.
7) El archivo SQLite se infla y nunca se reduce
Síntomas: Uso de disco crece incluso tras deletes; VPS se queda sin disco.
Causa raíz: SQLite reutiliza páginas pero no siempre devuelve espacio al sistema de archivos; fragmentación; deletes grandes.
Solución: VACUUM periódico (costoso); diseña una estrategia de retención; considera dividir tablas grandes o migrar a Postgres si el churn es alto.
8) “Usamos Postgres porque es enterprise” y ahora operaciones se ahogan
Síntomas: Nadie se ocupa de upgrades, vacuum, backups; la BD es una mascota, no ganado.
Causa raíz: Elegir Postgres sin asignar madurez operativa.
Solución: Invierte en lo básico de operaciones (monitorización, drills de backup, cadencia de upgrades) o mantén la simplicidad con SQLite hasta que realmente necesites la DB servidor.
Tres microhistorias corporativas
Microhistoria 1: El incidente causado por una suposición errónea (archivo SQLite en “almacenamiento compartido”)
La compañía era mediana, el producto sano, y a alguien se le ocurrió una idea brillante: ejecutar dos instancias de la app tras un balanceador “para resiliencia”. La base de datos era SQLite, situada en lo que el proveedor de VPS anunciaba como “almacenamiento compartido”, montado en ambas instancias. Parecía elegante. Un archivo. Dos instancias. ¿Qué podría salir mal?
Funcionó unos días. Luego llegó el primer pico de tráfico—nada dramático, solo un correo de marketing. Las peticiones empezaron a acumularse. La latencia se disparó. Algunos usuarios obtuvieron errores; otros, lecturas obsoletas; unos cuantos vieron actualizaciones parciales que desaparecían al refrescar.
El on-call revisó logs y encontró “database is locked” intermitente, pero no constante. Peor aún, aparecieron mensajes ocasionales tipo “disk I/O error” que parecían hardware. No lo eran. Eran el sistema de archivos y el gestor de bloqueos teniendo un desacuerdo sobre quién poseía la verdad entre dos nodos.
La suposición errónea fue sutil: “Si el almacenamiento es compartido, el bloqueo de archivo es compartido”. En muchos sistemas de archivos compartidos, los locks de asesoramiento no se comportan como los locks locales ext4, especialmente bajo fallos o latencia. SQLite no estaba “roto”; el entorno violó las asunciones que hace para ofrecer semánticas ACID.
La solución fue aburrida: migrar a Postgres en un nodo primero, luego añadir una réplica más tarde. También eliminaron el montaje compartido y trataron los límites de almacenamiento como límites de fallo. El informe del incidente no culpó a SQLite; culpó a la arquitectura que fingió que un archivo podía ser un sistema distribuido.
Microhistoria 2: La optimización que salió mal (Postgres afinado para velocidad, pagado en ansiedad por pérdida de datos)
Otra organización tenía Postgres en un VPS pequeño. Las escrituras eran intensas: eventos, logs, contadores. El equipo quería menor latencia y vio un post que sugería apagar perillas de durabilidad. Cambiaron ajustes para reducir la presión de fsync y hacer que los commits retornaran más rápido. Todos aplaudieron. Los gráficos bajaron y todo parecía perfecto.
Dos semanas después el host del VPS tuvo un reinicio no planificado. Nada dramático—uno de esos “mantenimientos de nodo” que solo conoces después. Postgres reinició bien, pero un trozo de las escrituras más recientes faltaba. No catastrófico, pero suficiente para generar preguntas de clientes y alarmas internas.
Entonces llegó el impuesto real: la incertidumbre. No podían decir con confianza qué se había perdido, y el equipo de producto empezó a tratar la base de datos como “posiblemente consistente”. Eso es corrosivo. Convierte cada bug en un debate sobre si los datos son reales.
La optimización salió mal porque optimizó lo incorrecto: latencia en estado estable a costa de durabilidad predecible. Hay razones válidas para relajar durabilidad para analítica efímera o caches. Pero ellos la usaban para estado orientado al cliente.
La solución fue restaurar ajustes seguros para tablas core, aislar datos de alto volumen y bajo valor en rutas separadas, y ejecutar backups con pruebas de restauración. También introdujeron batching para reducir la frecuencia de commits en lugar de apostar por el comportamiento ante fallos.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día (drills de backup y automatización de restauración)
Esta es menos dramática, y ese es el punto. Un equipo que ejecutaba un SaaS en un VPS usaba Postgres. No eran sofisticados. No tenían equipo de plataforma. Pero hacían una cosa sin descanso: drills de restauración semanales a una VM de pruebas, con una lista de verificación.
Tenían un script que bajaba el backup más reciente, lo restauraba, ejecutaba un pequeño conjunto de consultas de sanity y confirmaba que la app podía arrancar contra él. También mantenían un “runbook” mínimo que describía cómo promover la BD restaurada si la primaria moría. A nadie le encantaba hacerlo. Era como pasar hilo dental.
Luego un desarrollador ejecutó por error una migración destructiva contra producción. No malintencionado. Solo una variable de entorno mal puesta y una herramienta de migración obediente. El on-call silenció alertas, juró en voz baja y empezó el drill de restauración que habían practicado.
Todavía tuvieron una mala hora, pero no una mala semana. Restauraron, reenviaron migraciones correctamente y reprodujeron una ventana corta de eventos de negocio desde logs. El CEO nunca tuvo que aprender qué significa “WAL”, lo cual es el mayor cumplido que pueden recibir las operaciones.
Cita (idea parafraseada): “No te elevas a la ocasión; vuelves a tu preparación.” — idea parafraseada común en círculos de fiabilidad/ops
Listas de verificación / plan paso a paso
Lista A: Si te inclinas por SQLite (dále forma de producción)
- Confirma la realidad de un solo escritor: lista todas las rutas de código que escriben (peticiones web, workers, cron, scripts admin). Si hay más de un actor a la vez, planea serializar o migrar.
- Usa modo WAL: establece
PRAGMA journal_mode=WAL. - Establece synchronous sensato: generalmente
NORMALes un buen compromiso para VPS; usaFULLsi no toleras pérdida de escrituras recientes tras un crash. - Configura busy_timeout: haz que la app espere brevemente en lugar de fallar instantáneamente por contención de locks.
- Respaldos correctos: usa el mecanismo de backup de SQLite, no “cp del archivo en horas pico”.
- Planifica crecimiento de archivo: monitoriza tamaño de BD y disco libre; programa VACUUM periódico solo si es necesario.
- No pongas SQLite en NFS/montajes compartidos: disco local únicamente, salvo que disfrutes depurar bloqueos de archivos con latencia.
Lista B: Si te inclinas por PostgreSQL (hazlo aburrido, estable y barato)
- Tamaño de conexiones adecuado: mantén
max_connectionssensato; usa un pooler para apps web. - Configura memoria deliberadamente: ajusta
shared_buffersde forma conservadora en RAM pequeña; deja margen para caché del SO y tu app. - Habilita visibilidad de consultas: activa estadísticas de consultas para ver qué es lento antes de que los usuarios lo digan.
- Monitoriza vacuum: observa tuplas muertas y actividad de autovacuum; el bloat es una fuga lenta.
- Backups y pruebas de restore: automatiza ambos. Un backup sin prueba de restauración es un deseo.
- Planificación de actualizaciones: decide cómo manejarás updates menores y upgrades de versión antes de que te obliguen.
- Gestión de disco: monitoriza uso de disco para datos y WAL; evita operar al 90% en un VPS.
Paso a paso: la ruta de decisión sin remordimientos (15 minutos)
- Ejecuta Task 1–4 para entender la realidad de RAM e IO.
- Lista tus escritores. Si hay más de un escritor concurrente ahora o pronto, elige Postgres.
- Si SQLite sigue siendo plausible, ejecuta Task 7–8. Si aparece contención de locks en una prueba de juguete, elige Postgres.
- Si eliges Postgres, ejecuta Task 9–12 y confirma que puedes mantenerlo saludable en este VPS.
- Ejecuta Task 15 y realiza al menos un drill de restauración. Elige el sistema cuya ruta de restauración puedas ejecutar bajo estrés.
Broma #2: La base de datos más rápida es la que no perdiste a las 03:00, que es también por qué los backups tienen el mejor ROI de cualquier característica que nunca vas a demostrar.
Preguntas frecuentes
1) ¿Puede SQLite manejar tráfico de producción?
Sí, si “tráfico de producción” significa mayormente lecturas, un número pequeño de escrituras y un modelo de concurrencia controlado. Se usa en muchos sistemas reales. Simplemente no quiere ser tu coordinador de escrituras multi-tenant.
2) ¿El modo WAL hace a SQLite “tan bueno como Postgres”?
No. WAL reduce bloqueo lector/escritor y mejora la concurrencia, pero aún tienes un único archivo de base de datos con semánticas de bloqueo y menos herramientas de concurrencia. Postgres está diseñado como servicio compartido.
3) ¿Es Postgres excesivo para un VPS pequeño?
A veces. Si tu VPS es diminuto y tu carga es simple, Postgres puede ser piezas móviles de más. Pero si tienes múltiples escritores o trayectoria de crecimiento, lo “excesivo” rápidamente se convierte en “gracias por evitarme migrar bajo presión”.
4) ¿Cuál es el mayor coste oculto de Postgres en un VPS?
Gestión de conexiones y memoria. Sin pooling y límites sensatos, Postgres puede quemar RAM con sesiones inactivas y morir de una forma que parece “inestabilidad aleatoria”. No es aleatorio; es matemáticas.
5) ¿Cuál es el mayor coste oculto de SQLite en un VPS?
Contención por locks y supuestos operativos. En el momento que tienes múltiples escritores, transacciones largas, o pones el archivo en almacenamiento cuestionable, heredas modos de fallo que se sienten misteriosos hasta que aceptas el modelo de bloqueo.
6) Si empiezo con SQLite, ¿qué tan dolorosa es la migración a Postgres?
Va desde “un fin de semana” hasta “un trimestre”, dependiendo de la complejidad del esquema, volumen de datos y cuánto tu app dependa de rarezas de SQLite. Si prevés crecimiento, diseña tu app con una abstracción de BD y herramientas de migración desde el día uno.
7) ¿Debería usar SQLite para cache y Postgres como fuente de la verdad?
Puedes, pero no construyas un sistema distribuido accidentalmente. Si necesitas cache, considera caches en memoria o estrategias nativas de Postgres. Si usas SQLite como cache local, trátalo como descartable y reconstruible.
8) ¿Y la durabilidad: es SQLite inseguro?
SQLite puede ser duradero cuando se configura correctamente y se usa en un sistema de archivos que respete sus expectativas. El riesgo no es “SQLite es inseguro”, es “SQLite facilita que seas inseguro sin notarlo”. Postgres centraliza esos comportamientos de durabilidad en un servidor diseñado para caídas.
9) ¿Necesito replicación en un VPS?
No siempre. Para muchos despliegues en VPS, la primera victoria es backups fiables y drills de restauración. La replicación vale la pena cuando tus requisitos de uptime exceden “restaurar en X minutos” y puedes permitirte la complejidad.
10) ¿Cómo decido si mi app tiene “múltiples escritores”?
Si las escrituras pueden ocurrir concurrentemente desde más de un proceso OS o contenedor (workers web, job workers, tareas programadas, scripts admin), tienes múltiples escritores. Si despliegas múltiples instancias de la app, definitivamente los tienes.
Siguientes pasos que puedes hacer hoy
Elige un camino y hazlo operable en la realidad. Las bases de datos no fallan porque elegiste la marca equivocada; fallan porque no emparejaste el sistema con la carga y no practicaste la recuperación.
Si eliges SQLite
- Habilita WAL y establece synchronous explícitamente.
- Añade busy timeout y mantén transacciones cortas.
- Implementa backups usando el mecanismo de backup de SQLite y realiza una prueba de restauración.
- Escribe una regla clara: “no filesystem compartido, no caos multi-writer”.
Si eliges PostgreSQL
- Configura pooling de conexiones y límites sensatos de inmediato.
- Activa visibilidad de consultas y vigila consultas lentas y bloqueos.
- Automatiza backups y realiza drills de restauración según un calendario.
- Monitoriza uso de disco y salud de vacuum antes de que sea necesario.
La edición sin remordimientos no trata de elegir la “mejor” base de datos. Se trata de elegir la base de datos cuyos modos de fallo puedas predecir, observar y recuperar en un VPS durante horas humanas.