SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-07

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

Descubre cuándo SQLite encaja en un VPS, cómo configurar WAL y busy_timeout, usar Litestream y reconocer el límite de un solo escritor.

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, y el motivo es sencillo: 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 función y no un recorrido de red, por lo que una página que ejecuta cuarenta consultas supone cuarenta llamadas a función.

El límite es concreto y real. SQLite permite un solo escritor a la vez en todo el archivo de 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 determinantes en cuanto la aplicación 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 que se muestra a continuación se ejecutó en Ubuntu 24.04.

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

Esto muestra una versión que comienza 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 utiliza 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 el controlador de base de datos antes de depender de una función reciente.

Por qué el modo WAL es el primer cambio que debe hacer

De forma predeterminada, SQLite usa un diario de reversión. Antes de modificar una página, copia la página original en un archivo -journal y después edita la base de datos directamente. 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 bloquea todas las peticiones 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 en la instantánea que tenían al comenzar. Por tanto, los lectores no bloquean al escritor y el escritor no bloquea a los lectores. Más adelante, un checkpoint copia las páginas acumuladas en WAL de vuelta a la base de datos principal. Este único cambio es lo que más contribuye a que SQLite sea utilizable detrás de una aplicación web.

Activa el modo WAL y confirma que se mantiene

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

El comando muestra 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ébalo con una conexión nueva.

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

Ahora crea una tabla y comprueba qué archivos aparecen 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 cada conexión asigna para que todas sepan qué contiene el WAL. Ambos pertenecen a la base de datos y no son archivos temporales. Si copias app.db por separado mientras la aplicación está en ejecución, obtienes un archivo al que le faltan todas las confirmaciones recientes. Si eliminas app.db y dejas los otros dos archivos en su lugar, SQLite aplicará esas páginas WAL obsoletas al archivo nuevo que aparezca con ese nombre. Así es como se corrompe una base de datos nueva al intentar restablecerla.

Los ajustes de conexión que toda aplicación en producción necesita

Sólo journal_mode se almacena en la base de datos. Todos los demás ajustes siguientes son específicos de cada conexión. Esto significa que la aplicación debe ejecutarlos 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 reintentando cuando la base de datos esté 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 el ajuste adecuado en modo WAL, y conviene entender la compensación. Con FULL, SQLite llama a fsync en el WAL después de cada commit. Con NORMAL, sincroniza durante los checkpoints. La documentación de SQLite explica claramente qué se pierde: las transacciones dejan de ser duraderas después de un corte de alimentación o un reinicio forzado. La base de datos no se corrompe por esa pérdida de alimentación. Simplemente se pierden los últimos commits que todavía no habían llegado al disco. En un VPS, esta suele ser la compensación adecuada, porque elimina un fsync de la ruta 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 este ajuste.

Hay otro ajuste que sólo importa más adelante. SQLite ejecuta checkpoints automáticamente cuando el WAL supera las 1000 páginas, y la operación la realiza la conexión que casualmente termina una transacción en ese momento. 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 checkpoints.

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

Este es el fallo que hace que muchos usuarios vuelvan a Postgres, y tiene una causa concreta.

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

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 se produce cuando una transacción cambia de tipo. Un BEGIN sin más en SQLite significa BEGIN DEFERRED. Si la primera instrucción posterior es un SELECT, está en una transacción de lectura. Si 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á obsoleta y la espera sólo 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 ni siquiera se consulta. El error llega inmediatamente, por eso parece que la configuración no ha tenido efecto.

La solución es una sola palabra.

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

BEGIN IMMEDIATE adquiere el bloqueo de escritura al principio, antes de leer nada. No hay ninguna actualización de tipo, por lo que no hay ningún interbloqueo que evitar. El controlador busy se aplica y la conexión espera su turno en lugar de fallar. Mantenga diferidas las transacciones de sólo lectura. Toda transacción que contenga 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 un trabajo lento. SQLite serializa las escrituras, por lo que una transacción que se abre, llama a una API externa a través de la red y después confirma bloqueará a todos los demás escritores durante toda esa llamada. Lea lo necesario, cierre la transacción, realice el trabajo lento y después abra una transacción de escritura breve para guardar el resultado.

Copias de seguridad continuas con Litestream

Una copia nocturna puede perder hasta un día de escrituras, y ejecutar cp contra una base de datos SQLite activa puede producir una copia que no se pueda 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 los cambios al almacenamiento de objetos de forma continua. 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 igual que 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. Cambie la versión en ambas líneas para que coincida con la etiqueta actual de la página de versiones y use el paquete arm64 correspondiente si su VPS utiliza arm64.

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

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

Tenga 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 durante el arranque. Muchas guías de terceros todavía muestran el array antiguo, así que copie la estructura anterior en lugar del primer ejemplo que aparezca en una búsqueda. La serie 0.5 también cambió el nombre del subcomando litestream wal por litestream ltx, porque cambió el formato de las copias de seguridad almacenadas en disco.

Compruebe que la configuración se pueda analizar antes de habilitar nada.

sudo litestream databases -config /etc/litestream.yml

Después, compruebe manualmente el ciclo 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 otra shell, escriba una fila y restaure 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 envía los cambios según un sync-interval cuyo valor predeterminado es 1 segundo, así que espere y vuelva a restaurar. Ese segundo también es su punto de recuperación. Un bloqueo puede perder como máximo las escrituras del último intervalo de sincronización, y ninguna configuración puede reducirlo a cero.

Para el almacenamiento real, sustituya el bloque de réplica por una URL de S3. Esto 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 incluya las credenciales en ese archivo. Litestream lee LITESTREAM_ACCESS_KEY_ID y LITESTREAM_SECRET_ACCESS_KEY desde el entorno, así que colóquelas en un drop-in de systemd propiedad de root con permisos 600.

Los valores de las instantáneas anteriores son los predeterminados, pero el valor de retención suele causar confusión. La retención indica durante cuánto tiempo Litestream conserva las instantáneas y los archivos asociados, por lo que también determina hasta qué momento anterior se puede restaurar. Veinticuatro horas significa que una migración incorrecta detectada el miércoles por la mañana ya no se puede recuperar usando el estado del lunes. Configure retention: 168h para una semana y asuma el coste del almacenamiento adicional.

Compruebe 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 otro resultado indica que la copia restaurada no se puede utilizar. Ejecútelo según un calendario con un servicio y un temporizador de systemd y revise el resultado. Hasta que no haya restaurado una copia de seguridad al menos una vez, no puede saber 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

Una salida correcta muestra cada base de datos definida en la configuración y después permanece en silencio, salvo por las líneas de sincronización periódicas. Un error de no such file or directory en la ruta de la base de datos indica que la ruta de la configuración es incorrecta o que el proceso no puede leerla. De forma predeterminada, la unidad se ejecuta como root, 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 situados junto a la base de datos. Por tanto, asígnale la cuenta que ya utiliza 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 sólo lleva unos minutos. Esto marca la diferencia entre un agente de copias de seguridad y un segundo proceso ejecutándose como root en el servidor.

Hay un detalle de orden importante 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. Añádelo 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 todo 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 resolver 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, proporcionada por el archivo -shm. La documentación de SQLite establece esta regla sin excepciones:

Todos los procesos que usan una base de datos deben estar en el mismo equipo host; WAL no funciona sobre 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 SMB puede corromperse, y 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, y eso no supone un problema. Un recurso compartido de archivos montado es distinto.

Un segundo servidor de aplicaciones. Ninguna opción permite hacerlo funcionar. Cuando necesita dos máquinas que sirvan los mismos datos, necesita una base de datos que se comunique a través de la red. Tome esa decisión mientras todavía tenga tiempo para planificar la migración.

Cargas de trabajo con muchas escrituras. Tener un solo proceso de escritura a la vez es una propiedad del formato de archivo, no un parámetro ajustable. Las escrituras cortas son rápidas porque cada confirmación se añade al WAL, por lo que 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 el efecto de esa diferencia. El problema real son las transacciones largas, porque ponen en cola a todos los demás procesos de escritura.

Consultas analíticas. SQLite es un almacén orientado a filas diseñado para transacciones. Un panel que analiza cien millones de filas requiere otra herramienta para otro tipo de trabajo; DuckDB comparado con SQLite para trabajos de servidor explica dónde se encuentra ese límite.

VACUUM durante la replicación. Un VACUUM completo reescribe todo el archivo de la base de datos. Por eso Litestream debe volver a cargarlo entero, y la documentación de Litestream desaconseja ejecutarlo localmente 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 misma base de datos. No ejecute nunca dos procesos de Litestream sobre la misma base de datos ni sobre el mismo destino de réplica. La documentación indica explícitamente que evitarlo es responsabilidad del administrador, y el resultado es una réplica que no se puede restaurar.

Qué no cubre Litestream

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 con restic mediante una programación y cubrirá ambas partes. 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 por bloqueo y realice copias de seguridad de forma continua. Los límites importantes son estructurales: un escritor a la vez y un solo equipo host. Una aplicación que se mantenga dentro de esos límites obtiene una base de datos sin salto de red ni proceso independiente que monitorizar. Una aplicación que supere esos límites 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 espera cuando esperar 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ó entretanto, SQLite devuelve SQLITE_BUSY inmediatamente en lugar de llamar al gestor de espera, porque la instantánea de lectura ya está obsoleta. Inicie con BEGIN IMMEDIATE cualquier transacción que vaya a escribir, para que el bloqueo de escritura se tome 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 bloque 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 realizo 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 fallo provoca la pérdida aproximada del último segundo. También es más seguro que copiar el archivo de base de datos con cp, ya que esa copia puede capturar la base de datos durante una escritura. Litestream sólo cubre la base de datos, así que mantenga también una copia de seguridad general de archivos.

#sqlite#wal#litestream#backups#production