Sustituyendo Postgres por SQLite¶
8 minutos de lectura · 15 marzo 2026
El año pasado migré tres servicios de Postgres a SQLite. Dos siguen en producción y son los más estables que tengo. El tercero volvió a Postgres a los dos meses. Aquí va el porqué de los tres.
SQLite no es "Postgres pero más pequeño". Es una arquitectura distinta con un trade-off muy explícito.
El trade-off, en una línea¶
SQLite te da operación trivial (un archivo) a cambio de aceptar que solo escribes desde un único proceso a la vez.
Si tu carga real cabe en ese trato, ganas mucho. Si no, no hay magia que valga.
Los tres servicios¶
1. Generador de informes nocturno → sigue en SQLite¶
Lee de varias fuentes, agrega, escribe un informe por cliente. Una sola escritura nocturna. Lecturas concurrentes durante el día.
Fue donde el cambio fue más obvio. Eliminé el Postgres dedicado, su pgbouncer, sus backups custom y su monitorización. Ahora es un archivo informes.db versionado con litestream a S3. Recuperación: bajar el archivo y arrancar. Coste mensual: cero infraestructura.
Por qué funcionó: el patrón de carga era literalmente para lo que SQLite está diseñado.
2. API interna con cachés → sigue en SQLite¶
Servicio HTTP que sirve catálogo (lectura masiva) y se actualiza una vez por hora con un job. Treinta mil requests/min en pico.
WAL mode + PRAGMA mmap_size=30000000000 y SQLite atiende esa carga sin sudar. Con --threads 4 en el binary, el bottleneck eran las CPUs de la VM, no la DB.
Detalle clave: writes solo desde el job nocturno → cero contención de escritura.
3. Servicio de gestión de pedidos → volvió a Postgres¶
Multi-tenant. Cada pedido es una escritura. Diez tenants concurrentes. Pareció ir bien dos semanas.
Cuando un tenant se puso pesado (300 escrituras/min), las escrituras de los otros tenants se ralentizaban. El lock global de SQLite es de toda la base de datos, no de la fila ni de la tabla. BEGIN IMMEDIATE resolvía las llamadas más sucias pero no la cola.
Lección: "una única conexión de escritura" significa exactamente eso. Si tu modelo asume escrituras concurrentes de verdad, SQLite no es el sitio.
La heurística que uso ahora¶
Para cada servicio nuevo, antes de decidir DB:
- ¿Cuántas escrituras/segundo en pico? Si <50, SQLite es default.
- ¿Hay un único escritor o varios? Si hay varios genuinos, Postgres.
- ¿Necesito replicación lógica para HA? Si sí, Postgres. Si me basta con recovery,
litestream+ SQLite. - ¿Hay tipos avanzados (JSONB con índices GIN, geo)? Si los necesito de verdad, Postgres.
Es asombrosa la cantidad de servicios que pasan los cuatro filtros a SQLite.
Lo que se gana operacionalmente¶
- Cero infraestructura. No hay un servicio que mantener separado, no hay credenciales que rotar, no hay pgbouncer.
- Backups triviales. Copiar el archivo (en WAL + checkpoint) o
litestreampara continuo. - Test isolation perfecto. Cada test crea su DB en memoria. Sin docker-compose de Postgres en CI.
- Migraciones simples.
alembico equivalentes funcionan; pero como no hay carga concurrente, son menos arriesgadas. - Deploy sin coordinación de DB. Versión nueva del código + archivo viejo = funciona.
Lo que se pierde¶
- Lecturas paralelas son perfectas; escrituras paralelas no existen.
EXPLAINde SQLite es honesto pero modesto.- Extensiones (PostGIS, pgvector serio) no tienen equivalente.
- Tooling de observabilidad es escaso. No hay
pg_stat_statements.
El error que cometí¶
Lo conté mal a mi equipo al principio. Lo vendí como "más simple, igual de capaz". No es igual de capaz. Es más simple para casos concretos. Cuando salió el caso del servicio multi-tenant, gasté tres semanas peleando con SQLite cuando debí migrar a Postgres en una tarde.
Si fuera a venderlo a un equipo otra vez:
"SQLite es la mejor base de datos del mundo para una sola escritura concurrente. Postgres es la mejor para múltiples. Elige según ese eje, no según moda."
Próxima entrada
Mi configuración exacta de WAL + mmap + litestream para producción, con los pragmas que uso y los que evito. En la próxima.
¿Comentarios o correcciones? info@encodigo.es.