SQLite com vários processos: "database is locked" e cópia corrompida
Saiba quantos escritores o SQLite aguenta, ative WAL e tempo de espera, faça backup consistente com .backup e reconheça quando o projeto precisa de um banco cliente-servidor.
Segunda de manhã, a agenda da clínica para de marcar consultas com o erro database is locked. Na sexta, alguém tinha colocado o arquivo agenda.db numa pasta sincronizada para "todo mundo ver pelo visualizador", e um script noturno passou a gravar no mesmo arquivo que o sistema. Na terça, a cópia que a sincronização levou para o outro computador abre pela metade.
A resposta curta: SQLite aceita muitos leitores, mas só um escritor por vez, e todos os processos precisam estar na mesma máquina, com o arquivo em disco local. Ative o modo WAL, configure um tempo de espera para o lock e use transações curtas. Nunca sincronize ou copie o arquivo com o banco aberto; faça backup com .backup ou VACUUM INTO. Se vários servidores precisam gravar, o projeto já pede um banco cliente-servidor.
Por que o arquivo não é uma planilha compartilhada
SQLite não tem servidor. Cada programa que abre o banco carrega a biblioteca e mexe diretamente no arquivo, coordenando-se com os outros por meio de locks do sistema operacional. Isso o torna rápido, simples e perfeito para um aplicativo, um celular ou uma ferramenta de linha de comando. Também define seus limites.
Para gravar, a conexão precisa de um lock exclusivo de escrita. Enquanto uma conexão escreve, as outras que querem escrever esperam. Se a espera passa do tempo configurado, ou se nenhum tempo foi configurado, a operação falha na hora com SQLITE_BUSY, que aparece como "database is locked".
Os locks dependem do sistema de arquivos. Em compartilhamento de rede, como SMB ou NFS, eles podem não funcionar direito, e a documentação do SQLite alerta que isso pode corromper o banco. O modo WAL, que é o que resolve boa parte da disputa, exige explicitamente que todos os processos estejam no mesmo computador.
O que mora ao lado do arquivo
Com WAL ativo, você verá dois arquivos extras: agenda.db-wal e agenda.db-shm. O primeiro guarda transações já confirmadas que ainda não foram transferidas para o arquivo principal. Copiar só o agenda.db significa perder essas transações. Copiar os três no meio de uma escrita significa pegar cada um num momento diferente. Por isso arrastar o arquivo no explorador ou deixar o Dropbox sincronizar não é backup.
Como configurar o SQLite para disputar menos
O exemplo é a agenda da clínica, em Python, com um único servidor web e um script noturno de lembretes, ambos na mesma máquina.
- Ative o WAL uma vez. O modo fica gravado no arquivo. Com ele, leitores não bloqueiam o escritor e o escritor não bloqueia leitores.
- Configure um tempo de espera em toda conexão, para que um lock momentâneo vire espera e não erro.
- Abra a transação de escrita já pedindo o lock, com
BEGIN IMMEDIATE, quando você vai ler e depois gravar. Isso evita que duas conexões leiam, decidam gravar e uma delas falhe no meio. - Mantenha a transação curta. Nada de chamar API externa ou esperar o usuário com a transação aberta.
- Feche conexões que não estão em uso, especialmente em scripts.
import sqlite3
def conectar(caminho="agenda.db"):
# timeout=5: espera até 5 segundos pelo lock antes de desistir
conn = sqlite3.connect(caminho, timeout=5, isolation_level=None)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
def marcar(conn, paciente, horario):
conn.execute("BEGIN IMMEDIATE")
try:
ocupado = conn.execute(
"SELECT COUNT(*) FROM consultas WHERE horario = ?", (horario,)
).fetchone()[0]
if ocupado:
conn.execute("ROLLBACK")
return False
conn.execute(
"INSERT INTO consultas (paciente, horario) VALUES (?, ?)",
(paciente, horario),
)
conn.execute("COMMIT")
return True
except Exception:
conn.execute("ROLLBACK")
raise
Com isolation_level=None, o módulo sqlite3 do Python não abre transações por conta própria, e você controla BEGIN, COMMIT e ROLLBACK explicitamente. É mais verboso e muito mais previsível.
Um servidor com vários threads ou workers na mesma máquina, configurado assim, aguenta bastante tráfego de leitura e um volume razoável de escrita. O gargalo aparece quando muitas escritas longas competem pelo mesmo lock.
Como fazer backup que abre depois
O SQLite tem uma API de backup online que copia o banco de forma consistente mesmo com o sistema rodando. A ferramenta de linha de comando a expõe no comando .backup. Desde a versão 3.27, também existe VACUUM INTO, que grava uma cópia compactada num arquivo novo.
# cópia consistente com o sistema no ar
sqlite3 /srv/clinica/agenda.db ".backup '/backup/agenda-2026-10-17.db'"
# alternativa: cópia compactada
sqlite3 /srv/clinica/agenda.db "VACUUM INTO '/backup/agenda-2026-10-17-b.db'"
# confira a cópia antes de confiar nela
sqlite3 /backup/agenda-2026-10-17.db "PRAGMA integrity_check;"
A última linha deve responder ok. No Python, o mesmo resultado sai de origem.backup(destino), método das conexões do módulo sqlite3. Agende o comando, guarde as cópias fora da máquina e teste uma restauração de vez em quando, pelo mesmo motivo explicado em backup de MySQL que você consegue restaurar: backup que nunca foi aberto é esperança, não cópia.
Visualizador é para cópia, não para o arquivo vivo
Ferramentas como o SQLite Viewer no navegador ou o DB Browser for SQLite são ótimas para olhar tabelas e montar um relatório. Abra nelas a cópia gerada pelo backup, nunca o arquivo que o sistema está usando. Um visualizador que segura uma transação aberta enquanto você navega pode deixar o sistema esperando lock; um que grava sem querer uma alteração muda dado de produção.
Quando trocar de banco
SQLite deixa de ser a escolha certa quando há mais de uma máquina gravando, quando o arquivo precisaria morar num compartilhamento de rede, ou quando o volume de escrita simultânea faz o "database is locked" voltar mesmo com WAL e tempo de espera. Nesses casos, um banco cliente-servidor como PostgreSQL ou MySQL resolve o problema na raiz, porque um único processo servidor coordena todos os acessos. O desenho de acesso remoto está em MySQL em outro servidor.
Erros comuns
- Arquivo em pasta sincronizada com o banco aberto: a sincronização copia no meio da escrita e o outro lado recebe um banco inconsistente ou corrompido.
- Banco em compartilhamento de rede usado por várias máquinas: os locks podem falhar silenciosamente e duas escritas simultâneas corrompem o arquivo.
- Nenhum tempo de espera configurado: qualquer lock de milissegundos vira erro para o usuário, em vez de uma espera imperceptível.
- Transação aberta durante chamada externa: o lock de escrita fica preso por segundos e todos os outros escritores acumulam erro.
- Backup copiando só o
.db: as transações que estavam no-walsomem da cópia sem aviso. - Abrir o arquivo de produção no visualizador: uma transação esquecida ou uma edição acidental afeta o sistema ao vivo.
Checklist
- Todos os processos que gravam estão na mesma máquina, com o arquivo em disco local.
PRAGMA journal_mode;respondewal.- Toda conexão tem tempo de espera configurado.
- Transações que leem e depois gravam começam com
BEGIN IMMEDIATE. - O backup usa
.backup,VACUUM INTOou a API de backup, nunca cópia de arquivo. - Cada cópia passa em
PRAGMA integrity_check. - O arquivo não está em pasta sincronizada nem em compartilhamento de rede.
- Relatórios e visualizadores usam a cópia, não o arquivo vivo.
Liste os processos que abrem o seu arquivo SQLite para gravar, incluindo scripts agendados e ferramentas de alguém da equipe. São todos da mesma máquina, e algum deles segura uma transação por mais de um segundo?
Referência: https://sqliteviewer.app/