SSD Nodes Learn 8GB de RAM — $66/año
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-02

SQLite en producción en un VPS: límites y ajustes

Configura SQLite en un VPS con WAL, busy_timeout y Litestream. Conoce el límite de un solo escritor y cuándo SQLite deja de ser adecuado.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Cuándo SQLite es la base de datos de producción adecuada en un VPS

Ejecutar SQLite en producción en un VPS es la opción adecuada para la mayoría de las aplicaciones pequeñas. La razón es simple: un proceso en una máquina que escribe en un archivo no necesita un servidor de bases de datos. No hay ningún daemon que supervisar, ningún puerto que proteger con el firewall, ninguna contraseña que rotar ni una segunda máquina que mantener activa. Una consulta es una llamada a una función, no un recorrido de ida y vuelta por la red. Por tanto, una página que ejecuta cuarenta consultas tiene el coste de cuarenta llamadas a funciones.

El coste es específico y real. SQLite permite un solo escritor a la vez en todo el archivo de la base de datos, y el archivo no se puede compartir entre dos máquinas. Ambos límites son adecuados para un solo VPS que ejecuta una sola aplicación. Ambos se vuelven críticos en cuanto se supera ese modelo. Esta guía cubre los ajustes que hacen que SQLite sea seguro en un servidor, las copias de seguridad continuas con Litestream y el momento en que debe dejar de usarlo.

Instale primero la herramienta de línea de comandos. Todo lo siguiente se ejecutó en Ubuntu 24.04.

sudo apt update
sudo apt install -y sqlite3
sqlite3 --version

Esto muestra una versión que empieza por 3., seguida de una fecha de compilación y un hash del código fuente. Ubuntu 24.04 incluye SQLite 3.45.1 a fecha de julio de 2026. Probablemente su aplicación no usa este binario: la mayoría de los runtimes de lenguajes incluyen su propia copia de la biblioteca SQLite, a menudo más reciente. Compruebe la versión que informa su controlador de base de datos antes de depender de una función reciente.

Por qué el modo WAL es lo primero que se cambia

De forma predeterminada, SQLite usa un diario de reversión. Antes de cambiar una página, copia la página original en un archivo -journal y después modifica la base de datos en el mismo lugar. Para hacerlo de forma segura, adquiere un bloqueo exclusivo sobre todo el archivo. Por eso, todos los lectores esperan mientras hay una escritura en curso. En un portátil, nadie lo nota. En un servidor web, una escritura lenta retrasa todas las solicitudes que acceden a la base de datos.

El modo WAL (write-ahead log) invierte el orden. Un escritor añade las páginas nuevas a un archivo -wal independiente y deja intacta la base de datos principal. Los lectores siguen leyendo el archivo principal desde la instantánea con la que comenzaron. Así, los lectores no bloquean al escritor y el escritor no bloquea a los lectores. Después, un checkpoint copia las páginas acumuladas en WAL de vuelta a la base de datos principal. Este cambio es lo que más contribuye a que SQLite sea utilizable detrás de una aplicación web.

Activar el modo WAL y confirmar que se mantiene

mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"

El comando imprime wal. Esa salida no es decorativa. PRAGMA journal_mode devuelve el modo en el que está realmente la base de datos, por lo que una respuesta delete significa que el cambio falló y que todavía se usa el registro de reversión.

El modo WAL es persistente. Es una marca en la cabecera de la base de datos, no una configuración de la conexión. Por eso se ejecuta una vez por archivo de base de datos y todas las conexiones posteriores lo heredan, incluso después de reiniciar el sistema. Compruébelo con una conexión nueva.

sqlite3 ~/app/app.db "PRAGMA journal_mode;"

Ahora cree una tabla y compruebe qué aparece en el disco.

sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/

Ahora hay tres archivos: app.db, app.db-wal y app.db-shm. El archivo -wal contiene páginas confirmadas que todavía no se han sometido a un checkpoint. El archivo -shm es un índice de memoria compartida que todas las conexiones asignan para coincidir en el contenido del WAL. Ambos pertenecen a la base de datos y no son archivos temporales. Si copia app.db por separado mientras la aplicación está en ejecución, obtiene un archivo al que le faltan todas las confirmaciones recientes. Si elimina app.db y deja los otros dos archivos en su lugar, SQLite aplicará esas páginas obsoletas del WAL a cualquier archivo nuevo que aparezca con ese nombre. Así es como se corrompe una base de datos nueva al intentar restablecerla.

La configuración de conexión que necesita toda aplicación en producción

Solo journal_mode se almacena en la base de datos. Cada una de las demás configuraciones es específica de la conexión. La aplicación debe ejecutarla en cada conexión que abra, incluidas todas las conexiones que un pool cree en segundo plano.

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;

busy_timeout = 5000 indica a SQLite que siga intentando acceder a una base de datos bloqueada durante un máximo de 5000 milisegundos antes de devolver database is locked. El valor predeterminado es 0. Por tanto, SQLite falla de inmediato la primera vez que se solapan dos operaciones de escritura. Establecer este único valor elimina la mayoría de los errores de bloqueo que suelen atribuirse al propio SQLite.

synchronous = NORMAL es la configuración adecuada en modo WAL, pero conviene entender la contrapartida. Con FULL, SQLite llama a fsync en el WAL después de cada confirmación. Con NORMAL, sincroniza durante los puntos de control. La documentación de SQLite indica claramente qué se pierde: las transacciones dejan de ser duraderas después de un corte de alimentación o un reinicio forzado. Esa pérdida de alimentación no puede corromper la base de datos; simplemente se pierden las últimas confirmaciones que todavía no habían llegado al disco. En un VPS, normalmente es una contrapartida adecuada, porque elimina un fsync del proceso de cada escritura.

foreign_keys = ON está desactivado de forma predeterminada por compatibilidad con versiones anteriores y es específico de cada conexión. Un esquema lleno de cláusulas REFERENCES no aplica ninguna restricción hasta que cada conexión activa esta opción.

Hay otra configuración que solo importa más adelante. SQLite crea puntos de control automáticamente cuando el WAL supera las 1000 páginas. La operación la realiza la conexión que, en ese momento, termina una transacción. Esto no supone ningún problema por sí solo. Se convierte en una cuestión relevante cuando se ejecuta Litestream, porque Litestream necesita controlar cuándo se realizan los puntos de control.

Por qué database is locked sigue ocurriendo después de configurar busy_timeout

Este es el error que hace que algunas personas vuelvan a Postgres. Tiene una causa específica.

Un tiempo de espera de busy instala un controlador busy, pero SQLite no garantiza que vaya a llamarlo.

Si SQLite determina que invocar el controlador busy podría provocar un interbloqueo, devuelve SQLITE_BUSY a la aplicación en lugar de invocar el controlador busy.

El interbloqueo que evita ocurre cuando una transacción cambia de modo. Un BEGIN sin opciones en SQLite significa BEGIN DEFERRED. Si la primera instrucción posterior es un SELECT, la transacción es de lectura. Cuando un UPDATE posterior de esa misma transacción necesita convertirse en una transacción de escritura y otra conexión ha escrito desde que comenzó la lectura, SQLite no puede hacerle esperar. La instantánea ya está desactualizada y esperar solo provocaría un interbloqueo entre ambas conexiones. La documentación indica directamente el resultado:

Las instrucciones de escritura posteriores actualizarán la transacción a una transacción de escritura si es posible o devolverán SQLITE_BUSY.

El tiempo de espera de 5000 milisegundos nunca se consulta. El error aparece de inmediato. Por eso parece que la configuración no tuvo ningún efecto.

La solución es una palabra.

BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;

BEGIN IMMEDIATE adquiere el bloqueo de escritura al inicio, antes de leer nada. No hay ningún cambio de modo. Por tanto, no hay ningún interbloqueo que evitar. El controlador busy sí se aplica y la conexión espera su turno en lugar de fallar. Mantenga diferidas las transacciones de solo lectura. Cualquier transacción que incluya una escritura debe ser inmediata.

La segunda causa de los errores de bloqueo es más difícil de detectar: mantener abierta una transacción de escritura durante una operación lenta. SQLite serializa las escrituras. Por tanto, una transacción que se abre, llama a una API externa a través de la red y después hace commit bloqueará a todos los demás escritores durante toda esa llamada. Lea lo necesario, cierre la transacción, realice la operación lenta y después abra una transacción de escritura breve para guardar el resultado.

Copia de seguridad continua con Litestream

Una copia nocturna pierde hasta un día de escrituras, y ejecutar cp contra una base de datos SQLite activa puede producir una copia que no se podrá abrir. Hay dos opciones seguras. sqlite3 app.db ".backup /path/to/backup.db" usa la interfaz de copia de seguridad en línea de SQLite y funciona con una base de datos en uso. Litestream va más allá: supervisa el WAL y envía continuamente los cambios al almacenamiento de objetos. Así, la pérdida máxima de datos pasa de un día a aproximadamente un segundo.

Litestream es un único binario de Go que se ejecuta junto a la aplicación. No se sitúa entre la aplicación y la base de datos. La aplicación escribe en SQLite exactamente como antes, y Litestream lee el WAL y carga los cambios.

cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream version

v0.5.14 es la versión que documenta la página oficial de instalación para Linux a fecha de julio de 2026, y v0.5.15 se publicó el 21 de julio de 2026. Cambia la versión en ambas líneas para que coincida con la etiqueta actual de la página de versiones, y usa el paquete arm64 correspondiente si tu VPS utiliza arm64.

El archivo de configuración se encuentra en /etc/litestream.yml. Empieza con una réplica en un archivo local, porque permite comprobar todo el proceso sin necesitar credenciales de la nube.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      type: file
      path: /var/backups/litestream/app

Ten en cuenta que el campo es replica, en singular. Litestream 0.5 sustituyó el array replicas de la serie 0.3 por un único bloque de réplica, y una configuración con dos entradas ahora falla al iniciar. Muchas guías de terceros todavía muestran el array antiguo, así que copia la estructura anterior en lugar del primer ejemplo que aparezca en una búsqueda. La serie 0.5 también cambió el subcomando litestream wal por litestream ltx, porque cambió el formato de las copias de seguridad en disco.

Comprueba que la configuración se analiza correctamente antes de activar nada.

sudo litestream databases -config /etc/litestream.yml

Después, comprueba manualmente el recorrido completo. Esta forma omite el archivo de configuración y replica una base de datos en una ruta.

mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app

El proceso se ejecuta en primer plano y continúa activo. En otro shell, escribe una fila y restaura la réplica en un archivo nuevo.

sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"

El recuento incluye la fila nueva. Si no la incluye, el cambio todavía no se ha sincronizado: Litestream realiza envíos según un sync-interval que tiene un valor predeterminado de 1 segundo, así que espera y vuelve a restaurar. Ese segundo también es tu punto de recuperación. Un bloqueo pierde como máximo las escrituras del último intervalo de sincronización, y ninguna configuración puede reducirlo a cero.

Para usar almacenamiento real, sustituye el bloque de réplica por una URL de S3. Funciona con Amazon S3 y con almacenamiento de objetos compatible con S3 de otros proveedores.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      url: s3://your-bucket-name/app
      region: us-east-1

snapshot:
  interval: 24h
  retention: 24h

No guardes las credenciales en ese archivo. Litestream lee LITESTREAM_ACCESS_KEY_ID y LITESTREAM_SECRET_ACCESS_KEY del entorno, así que colócalas en un drop-in de systemd propiedad de root con modo 600.

Los valores de las instantáneas anteriores son los predeterminados, y el valor predeterminado de retención suele sorprender. La retención indica durante cuánto tiempo Litestream conserva las instantáneas y los archivos asociados. También indica hasta qué momento anterior puedes restaurar. Veinticuatro horas significa que una migración incorrecta detectada el miércoles por la mañana ya no se puede recuperar desde el estado del lunes. Establece retention: 168h en una semana y asume el coste del almacenamiento adicional.

Demuestre la restauración antes de necesitarla

litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"

Dada una ruta de base de datos, litestream restore busca la réplica correspondiente en /etc/litestream.yml y la descarga. PRAGMA integrity_check muestra ok para un archivo correcto. Cualquier otra salida indica que la copia restaurada no se puede usar. Ejecute esta comprobación según un calendario mediante un servicio y un temporizador de systemd y revise la salida. Hasta que no haya restaurado una copia de seguridad al menos una vez, no sabrá si funciona.

Ejecutar Litestream con systemd

El paquete de Debian instala una unidad de litestream que lee /etc/litestream.yml.

sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f

El resultado correcto muestra cada base de datos de la configuración y luego permanece en silencio, excepto por las líneas de sincronización periódicas. Un error de no such file or directory contra la ruta de la base de datos indica que la ruta de la configuración es incorrecta o que el proceso no puede leerla. La unidad se ejecuta como root de forma predeterminada, lo que concede más privilegios de los necesarios para este trabajo. Litestream debe poder leer y escribir tanto la base de datos como el directorio que la contiene, porque trabaja con los archivos -wal y -shm ubicados junto a la base de datos. Por eso, asígnale la cuenta que ya usa la aplicación.

# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser

Aplícalo con sudo systemctl daemon-reload y sudo systemctl restart litestream. Configurar una cuenta de servicio dedicada con privilegios mínimos lleva unos minutos y marca la diferencia entre un agente de copias de seguridad y un segundo proceso con privilegios de root en el servidor.

Hay un detalle del orden que importa si alguna vez reconstruyes la máquina desde cero. La base de datos debe restaurarse antes de que se inicie la aplicación. litestream restore acepta -if-db-not-exists, que devuelve 0 cuando el archivo ya existe, por lo que es seguro ejecutarlo en cada arranque. Colócalo en una línea ExecStartPre de la unidad de la aplicación. Así, un VPS nuevo descarga la base de datos y uno existente no hace nada. litestream replicate tiene un indicador -restore-if-db-not-exists equivalente si prefieres mantenerlo en un solo lugar.

Dónde falla SQLite en un VPS

Sistemas de archivos de red. Este es el límite que no se puede evitar mediante configuración. El modo WAL requiere que todos los procesos que usan la base de datos compartan una pequeña región de memoria. El archivo -shm proporciona esa región. La documentación de SQLite establece la regla sin excepciones:

Todos los procesos que usan una base de datos deben estar en el mismo equipo host; WAL no funciona a través de un sistema de archivos de red.

Por tanto, una base de datos ubicada en un recurso NFS montado (sistema de archivos de red) o en un recurso compartido SMB puede corromperse. Ningún pragma lo evita. Aquí hay una diferencia que suele pasarse por alto. Un dispositivo de bloques de red, que es lo que la mayoría de los proveedores de VPS conectan como almacenamiento adicional, aparece en Linux como un disco normal con un sistema de archivos normal. Eso funciona correctamente. Un recurso compartido de archivos montado no.

Un segundo servidor de aplicaciones. Ninguna configuración hace que esto funcione. Cuando necesita dos máquinas que sirvan los mismos datos, necesita una base de datos que se comunique a través de la red. Planifique ese cambio mientras aún tenga tiempo para hacerlo.

Cargas de trabajo con muchas escrituras. La escritura de una operación a la vez es una propiedad del formato de archivo, no un parámetro ajustable. Las escrituras cortas son económicas porque cada confirmación se añade al WAL. Por eso, el rendimiento depende más de la latencia de las escrituras pequeñas del disco que de la CPU. Consulte NVMe frente a almacenamiento SSD SATA en un VPS para ver la diferencia. El problema real son las transacciones largas, porque ponen en cola a todas las demás escrituras.

Consultas analíticas. SQLite es un almacén orientado a filas diseñado para transacciones. Un panel que analiza cien millones de filas es un trabajo diferente para otra herramienta. DuckDB comparado con SQLite para tareas de servidor explica dónde está ese límite.

VACUUM durante la replicación. Un VACUUM completo reescribe todo el archivo de la base de datos. Por tanto, Litestream debe volver a cargarlo completo. La documentación de Litestream desaconseja ejecutarlo en el mismo lugar mientras la replicación está activa. Detenga el replicador, ejecute vacuum, vuelva a iniciarlo y espere una nueva instantánea completa.

Dos replicadores en una base de datos. Nunca ejecute dos procesos de Litestream contra la misma base de datos o el mismo destino de réplica. La documentación indica explícitamente que evitar esta situación es su responsabilidad. El resultado es una réplica que no se puede restaurar.

Lo que Litestream no cubre

Litestream protege el archivo de la base de datos y nada más. Los archivos cargados, la configuración de la aplicación, los certificados TLS (seguridad de la capa de transporte) y los archivos de unidad siguen siendo responsabilidad suya. Combínelo con copias de seguridad cifradas fuera del servidor mediante restic según una programación, y ambas partes quedarán cubiertas. Si la máquina es nueva, los primeros diez minutos en un VPS nuevo cubren la configuración de la cuenta de usuario y del firewall que esta guía da por hecha.

FAQ

¿SQLite es suficiente para una aplicación en producción?

Para una aplicación en un solo servidor, sí, siempre que active el modo WAL, establezca un tiempo de espera para operaciones ocupadas y realice copias de seguridad de forma continua. Los límites importantes son estructurales: solo puede haber un escritor a la vez y una sola máquina host. Una aplicación que se ajuste a esos límites obtiene una base de datos sin salto de red y sin un proceso independiente que supervisar. Una aplicación que no se ajuste necesita una base de datos cliente-servidor, y ningún ajuste puede cambiarlo.

¿Por qué sigo obteniendo database is locked después de establecer busy_timeout?

Porque SQLite omite el gestor de operaciones ocupadas cuando la espera podría provocar un interbloqueo. Una transacción que comienza con un BEGIN sin más es diferida: un SELECT inicial la coloca en una transacción de lectura y una escritura posterior debe actualizarla. Si otra conexión escribió mientras tanto, SQLite devuelve SQLITE_BUSY inmediatamente en lugar de llamar al gestor de operaciones ocupadas, porque la instantánea de lectura ya está obsoleta. Inicie con BEGIN IMMEDIATE cualquier transacción que vaya a escribir, de modo que el bloqueo de escritura se adquiera desde el principio y se aplique el tiempo de espera.

¿Puedo mantener mi base de datos SQLite en almacenamiento de red?

No en un sistema de archivos de red como NFS o SMB. El modo WAL necesita que todos los procesos compartan memoria mediante el archivo -shm, y la documentación de SQLite establece que todos los procesos que usan la base de datos deben estar en el mismo equipo host. Un dispositivo de bloques de red conectado por su proveedor es diferente: Linux ve un disco normal con un sistema de archivos normal, y SQLite funciona allí.

¿Necesito Litestream si ya ejecuto copias de seguridad nocturnas?

Depende de cuántos datos pueda permitirse perder. Un trabajo nocturno implica perder hasta veinticuatro horas de escrituras. Litestream sincroniza aproximadamente una vez por segundo, por lo que un bloqueo cuesta aproximadamente el último segundo. También es más seguro que copiar el archivo de la base de datos con cp, porque esa copia puede capturar la base de datos durante una escritura. Litestream solo cubre la base de datos, así que mantenga también una copia de seguridad general de archivos.

#sqlite#wal#litestream#backups#production