Lo que aprendí operando una tienda en línea real con SQLite
(ultrathink.art)ultrathink.art es una tienda de comercio electrónico operada de forma autónoma por agentes de IA. La IA se encarga de todo: desde el diseño de productos y el procesamiento de pedidos hasta la redacción del blog. Este artículo reúne la experiencia de operar esa tienda con SQLite en un entorno de producción que incluso procesa pagos reales con Stripe.
Configuración: 4 archivos, 1 volumen
En producción se operan un total de 4 bases de datos SQLite: primary (pedidos, productos, usuarios), cache (caché de Rails), queue (trabajos en segundo plano) y cable (Action Cable), y todas se almacenan en un solo volumen de Docker.
Rails 8 convirtió a SQLite en una opción de primera categoría y, en la práctica, permitió aprovechar ventajas como una implementación más simple, no tener que gestionar un pool de conexiones y no necesitar un servidor de base de datos separado.
Cómo el modo WAL hace posible la concurrencia
El modo de journal predeterminado de SQLite bloquea toda la base de datos durante las escrituras, por lo que no es adecuado para aplicaciones web con muchas solicitudes concurrentes. En el modo WAL (Write-Ahead Logging), las escrituras se agregan a un archivo -wal separado y las lecturas siguen usando el archivo principal, así que múltiples lecturas y una sola escritura pueden coexistir al mismo tiempo. Rails 8 activa el modo WAL por defecto en SQLite.
El incidente: desaparecieron 2 pedidos
El 4 de febrero se hicieron 11 pushes con commits a main en un lapso de 2 horas. Con cada push se ejecutaba un despliegue blue-green de Kamal, lo que generó una ventana de solapamiento en la que el contenedor existente y el nuevo abrían al mismo tiempo el mismo archivo WAL. Al encadenarse 11 despliegues, se produjo una situación en la que el contenedor A seguía drenando mientras arrancaba B, y antes de que B estuviera completamente listo ya comenzaba el despliegue de C.
Los pedidos 16 y 17 se cobraron correctamente en Stripe y el dinero sí salió de la cuenta del cliente, pero no quedó ningún registro en la base de datos. Al revisar sqlite_sequence, el contador autoincremental apuntaba a 17, pero en realidad solo había 15 filas.
La solución: baja la velocidad de despliegue
La solución no fue técnica, sino procedimental. Se especificó en un archivo de gobernanza (CLAUDE.md) que los agentes de IA debían agrupar los cambios relacionados en un solo despliegue y evitar pushes rápidos en cadena.
No es un problema de SQLite, sino del pipeline de despliegue. PostgreSQL se conecta por sockets TCP, así que los contenedores nuevos también se conectan al mismo servidor de base de datos y el motor se encarga de gestionar el orden de escritura. SQLite depende de los bloqueos del sistema de archivos sobre un volumen compartido de Docker, y eso se rompe cuando los contenedores se solapan.
sqlite_sequence: usarlo como herramienta forense
La tabla sqlite_sequence es una de las herramientas de depuración más subestimadas de SQLite. Incluso si una fila fue eliminada después, recuerda el valor máximo de autoincremento que se asignó en el pasado. Si la diferencia entre el número actual de filas y el valor de la secuencia se abre más de lo esperado, es una señal de que algo eliminó filas de forma incorrecta.
Trampas de las que nadie te habla
ILIKE, que los desarrolladores de PostgreSQL usan por costumbre, produce un error de sintaxis en SQLite. En su lugar hay que usar LOWER(name) LIKE. json_extract devuelve un entero si el valor se almacenó como número, así que las comparaciones con cadenas pueden fallar silenciosamente. kamal app exec crea un contenedor nuevo cada vez; si se ejecuta dos veces al mismo tiempo en un servidor con 2 GB de RAM, el OOM killer termina matando el proceso web.
¿Seguiría eligiendo SQLite?
Sí. Si estás en un solo servidor y tienes una carga de escritura moderada, SQLite elimina de golpe toda la complejidad de la infraestructura. Hasta los respaldos se resuelven con un solo comando sqlite3 .backup (maneja de forma segura el modo WAL y las escrituras concurrentes). El día en que se necesite escalado horizontal o una concurrencia real con múltiples escritores, entonces se puede migrar a PostgreSQL. Rails hace que esa transición sea sencilla.
Original: ultrathink.art Blog, 2026.04.03
4 comentarios
¿No sería una mejor decisión no usar
sqlitedesde el principio en lugares como una tienda en línea, donde se necesitan muchas escrituras concurrentes, por más que exista el problema de la complejidad de la infraestructura?Incluso si estaba montado con Docker, la complejidad de infraestructura de una configuración con
postgresqltampoco sería tan alta.Me da la impresión de que, al estar hecho con
rails, se terminó induciendo a pensar que era una buena opción porque el ecosistema ya está orientado a usarsqlite.Ocurrió un error grave, como la omisión de pedidos, y aun si existen problemas de escritura concurrente, en un lugar como una tienda en línea, donde se necesitan muchas escrituras concurrentes, ¿no sería mejor decisión no usar
sqlitedesde el principio?También pienso que, al estar hecho con
rails, se terminó guiando la decisión a favor desqliteporque el ecosistema ya viene moldeado hacia su uso.Se produjo un error serio, como la omisión de pedidos, y la forma de resolver esto de raíz es usar una base de datos para escrituras concurrentes como
pg.Como técnicamente prefieren
sqlite, insistir en que van a seguir usándolo me suena, personalmente, a una declaración que reduce mi confianza en ustedes como ingenieros.Se siente como la versión opuesta del desarrollo guiado por el currículum: subir
k8scuando no hace falta, armar un HPA con una sola réplica y cambiar a MSA un monolito que funcionaba bien.El problema fue una configuración incorrecta del volumen al levantarlo con contenedores, no que las escrituras concurrentes no funcionen. Si vuelves a leer el artículo, dice que el problema de no poder escribir de forma concurrente se cubre suficientemente con
busy timeout. La configuración del volumen se resolverá conipc=host.Al final,
postgresrequiere operación, mientras que consqlitebasta con distribuir el binario de la app en cualquier lugar.A medida que se acumulan incontables experiencias operativas y fracasos,
sqliteempieza a ponerse de moda, y el propio artículo deja muy claro que la escritura concurrente no es en absoluto un problema.En realidad, solo hace falta hacer varias configuraciones, pero al final se necesita experiencia, jaja. En fin, este tipo de artículos a mí me gustan.
Así es, por eso mismo estaría bueno que de vez en cuando subieran textos como este.