SQLite database is locked: como corrigir concorrência, WAL e transações longas
Publicado em 2026-08-29T20:13:00Z · atualizado em 2026-08-29T19:13:16+00:00
O erro **SQLite database is locked** aparece quando uma conexão precisa acessar o banco, mas outra mantém um bloqueio incompatível por tempo demais. A mensagem parece sugerir arquivo corrompido ou processo fantasma; na maioria dos projetos,
SQLite database is locked: como corrigir concorrência, WAL e transações longas
O erro SQLite database is locked aparece quando uma conexão precisa acessar o banco, mas outra mantém um bloqueio incompatível por tempo demais. A mensagem parece sugerir arquivo corrompido ou processo fantasma; na maioria dos projetos, a causa real é mais simples: transação de escrita longa, conexão compartilhada incorretamente, espera curta ou concorrência maior que o desenho suporta.
Este guia reproduz o erro, explica o que SQLITE_BUSY comunica e apresenta uma sequência segura de correções: fechar transações rapidamente, configurar timeout, usar uma conexão por fluxo de execução, avaliar o modo WAL e, quando necessário, serializar as escritas. Os exemplos usam Python e a biblioteca sqlite3 que já acompanha a linguagem.
Legenda: o SQLite aceita muitos leitores, mas a coordenação de escrita continua exigindo disciplina.
Antes de tudo: não apague os arquivos de journal ou WAL
Quando surge um arquivo banco.db-wal, banco.db-shm ou banco.db-journal, algumas pessoas tentam apagá-lo para “destravar”. Isso é arriscado. A documentação do modo WAL do SQLite explica que o WAL faz parte do estado persistente do banco enquanto está em uso. Separá-lo do arquivo principal pode eliminar transações confirmadas ou danificar a cópia.
Faça backup consistente antes de qualquer intervenção. Pare os processos que usam o banco e utilize as interfaces do SQLite para checkpoint, integridade e cópia. O erro de bloqueio não é autorização para excluir arquivos auxiliares.
Também não copie somente banco.db enquanto a aplicação está gravando em WAL. Para backup online, use a API de backup da biblioteca ou o comando .backup do shell oficial.
O que SQLITE_BUSY quer dizer
O SQLite traduz disputas de bloqueio para o código SQLITE_BUSY; bibliotecas frequentemente exibem “database is locked”. Isso não significa necessariamente que o arquivo ficou permanentemente preso. Significa que, naquele instante e dentro do tempo de espera configurado, a operação não obteve o bloqueio necessário.
SQLite é um banco embutido: as aplicações acessam arquivos diretamente, sem um servidor central arbitrando cada comando. Essa arquitetura é compacta e excelente para aplicações locais, protótipos, ferramentas e muitos sites. O compromisso é que processos e conexões precisam respeitar as regras de concorrência do arquivo.
No modo de journal tradicional, leitores e escritor podem bloquear etapas uns dos outros. No WAL, leitores podem continuar enquanto um escritor acrescenta mudanças ao log. Ainda assim, existe um escritor por vez. WAL melhora a convivência entre leitura e escrita; não transforma SQLite em um banco com múltiplos escritores paralelos.
Como reproduzir o erro de forma controlada
Crie reproduzir_lock.py em uma pasta de teste:
import sqlite3
db = "laboratorio.db"
with sqlite3.connect(db) as setup:
setup.execute("CREATE TABLE IF NOT EXISTS notas (id INTEGER PRIMARY KEY, texto TEXT)")
conexao_a = sqlite3.connect(db)
conexao_b = sqlite3.connect(db, timeout=0.1)
try:
conexao_a.execute("BEGIN IMMEDIATE")
conexao_a.execute("INSERT INTO notas(texto) VALUES (?)", ("mantendo lock",))
# A conexão A ainda não confirmou nem desfez a transação.
conexao_b.execute("INSERT INTO notas(texto) VALUES (?)", ("segunda escrita",))
conexao_b.commit()
finally:
conexao_a.rollback()
conexao_a.close()
conexao_b.close()
Ao executar python reproduzir_lock.py, a segunda escrita deve falhar após cerca de 100 milissegundos. O teste demonstra dois pontos: o bloqueio foi provocado por uma transação aberta e um timeout muito pequeno; o finally é necessário para liberar recursos mesmo com exceção.
Não rode esse experimento sobre uma base real. Use um arquivo descartável e mantenha os dados de produção fora do laboratório.
Correção número um: encurte a transação
A melhor correção costuma ser reduzir o trabalho feito entre BEGIN e COMMIT. Não abra uma transação, faça uma chamada HTTP, processe um arquivo grande e só então confirme. Durante essa espera, outra conexão pode ficar sem acesso à escrita.
Prepare os dados antes. Dentro da transação, execute apenas comandos necessários e finalize:
import sqlite3
linhas = [(f"nota {i}",) for i in range(1000)] # preparação fora da transação
with sqlite3.connect("app.db", timeout=5) as conn:
conn.executemany("INSERT INTO notas(texto) VALUES (?)", linhas)
O gerenciador de contexto confirma no sucesso e desfaz em caso de exceção. Use parâmetros ? em vez de concatenar valores no SQL. Além de segurança, executemany evita abrir e fechar mil transações independentes.
Transações muito pequenas também têm custo. O alvo não é confirmar cada linha; é agrupar um lote coerente sem segurar o escritor enquanto executa trabalho externo.
Configure timeout, mas não use espera como curativo
O timeout de sqlite3.connect define quanto a conexão espera antes de levantar a exceção. No SQL, PRAGMA busy_timeout cumpre papel semelhante em milissegundos. A API oficial sqlite3_busy_timeout descreve que o manipulador dorme repetidas vezes até atingir o limite e então devolve SQLITE_BUSY.
conn = sqlite3.connect("app.db", timeout=10.0)
conn.execute("PRAGMA busy_timeout = 10000")
Dez segundos podem absorver picos curtos. Porém, aumentar para cinco minutos apenas mascara uma transação travada e piora a experiência. Primeiro meça quanto duram as escritas e descubra quem mantém a conexão aberta. O timeout deve cobrir variações normais, não defeitos estruturais.
Registre a duração da transação, não apenas a duração de cada execute. O intervalo completo entre começo e fim é o que importa para os concorrentes.
Uma conexão por thread ou tarefa
Compartilhar o mesmo objeto de conexão entre threads sem disciplina pode produzir erros e estados difíceis de explicar. No Python, o padrão check_same_thread=True impede o uso da conexão em outra thread. Desativá-lo não cria sincronização automática; apenas transfere essa responsabilidade para a aplicação.
Prefira abrir uma conexão por thread, requisição ou unidade de trabalho, com vida curta e encerramento explícito:
def salvar_nota(texto):
with sqlite3.connect("app.db", timeout=5) as conn:
conn.execute("INSERT INTO notas(texto) VALUES (?)", (texto,))
Em servidores web, não mantenha uma transação aberta entre requisições. Em aplicações assíncronas, uma chamada síncrona longa ao SQLite também bloqueia o event loop; utilize a integração apropriada da sua pilha ou mova a operação para um executor controlado.
Feche cursores e conexões. Objetos abandonados para o coletor de lixo podem manter recursos por mais tempo do que o código sugere.
Quando ativar o modo WAL
O Write-Ahead Logging costuma ser útil quando há muitas leituras e escritas curtas. Ative uma vez na inicialização controlada e confira o resultado retornado:
with sqlite3.connect("app.db") as conn:
mode = conn.execute("PRAGMA journal_mode = WAL").fetchone()[0]
print("journal_mode:", mode)
WAL permite que leitores consultem uma versão consistente enquanto o escritor acrescenta mudanças ao log. Ele não elimina conflito entre dois escritores. Também pressupõe que os processos estejam na mesma máquina e compartilhem corretamente a memória auxiliar; a documentação oficial desaconselha seu uso em sistemas de arquivos de rede.
Legenda: o checkpoint transfere páginas confirmadas do WAL para o banco principal; ele faz parte da operação normal.
Monitore checkpoints e tamanho do WAL. Um leitor que permanece aberto por muito tempo pode impedir que o checkpoint avance completamente, fazendo o arquivo crescer. Não “resolva” isso apagando o WAL: encontre a leitura longa e finalize a conexão corretamente.
BEGIN DEFERRED, IMMEDIATE e EXCLUSIVE
Uma transação DEFERRED, padrão do SQLite, não adquire imediatamente o bloqueio de escrita; ela começa quando a primeira operação exige acesso. BEGIN IMMEDIATE tenta reservar a escrita no início. Assim, a espera ou falha acontece antes do trabalho intermediário, o que pode deixar o comportamento mais previsível em uma unidade que certamente escreverá.
with sqlite3.connect("app.db", timeout=5, isolation_level=None) as conn:
conn.execute("BEGIN IMMEDIATE")
try:
conn.execute("UPDATE contadores SET valor = valor + 1 WHERE nome = ?", ("jobs",))
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
Não troque todas as transações para IMMEDIATE sem medir. Uma transação que só lê não precisa disputar a vaga de escritor. EXCLUSIVE é ainda mais restritivo fora de WAL e raramente é a resposta para uma aplicação comum.
Serialize escritas quando a carga pede
Se dezenas de trabalhadores tentam escrever ao mesmo tempo, crie uma fila com um consumidor de escrita. Os produtores preparam eventos e o escritor agrupa lotes curtos. Isso reduz disputa e torna a pressão observável.
from queue import Queue
from threading import Thread
import sqlite3
fila = Queue(maxsize=1000)
def escritor():
with sqlite3.connect("app.db", timeout=10) as conn:
while True:
item = fila.get()
try:
if item is None:
return
conn.execute("INSERT INTO eventos(tipo, payload) VALUES (?, ?)", item)
conn.commit()
finally:
fila.task_done()
Thread(target=escritor, daemon=True).start()
Em uma aplicação real, trate falhas, confirme por lote, tenha sinal de encerramento e não coloque objetos não serializáveis na fila. O maxsize cria contrapressão: quando o banco não acompanha, os produtores desaceleram em vez de ocupar memória indefinidamente.
Use a ferramenta de fluxogramas do IATechNerds para desenhar produtores, fila, escritor e resposta. Essa visão costuma revelar transações atravessando etapas que não deveriam estar dentro delas.
Diagnóstico passo a passo em produção
Comece sem alterar parâmetros. Identifique todos os processos que abrem o arquivo e confirme se algum editor, tarefa agendada ou segunda instância ficou conectado. Registre começo, commit, rollback e duração de cada transação lenta. Verifique se há chamadas externas dentro da transação.
Depois confirme o modo:
PRAGMA journal_mode;
PRAGMA busy_timeout;
PRAGMA wal_checkpoint(PASSIVE);
Faça PRAGMA integrity_check; em uma cópia ou janela controlada para avaliar integridade; um bloqueio isolado não implica corrupção. Se o arquivo está em pasta sincronizada, compartilhamento de rede ou volume com semântica de bloqueio incomum, mova o teste para disco local.
Compare o volume: escritas por segundo, duração no percentil 95, filas e timeouts. Para explorar dados sem depender do Excel, consulte a biblioteca de SQL e as ferramentas do IATechNerds.
Observabilidade que encontra a transação culpada
Uma mensagem genérica no ponto da falha mostra a vítima, não necessariamente quem segurou o bloqueio. Adicione um identificador da unidade de trabalho e marque início e fim da transação. Registre o nome lógico da operação, a quantidade de linhas e a duração, mas não grave parâmetros sensíveis nem o SQL completo quando ele contiver dados pessoais.
import logging
import time
import sqlite3
def atualizar_lote(linhas, job_id):
inicio = time.monotonic()
try:
with sqlite3.connect("app.db", timeout=5) as conn:
conn.executemany(
"UPDATE itens SET status = ? WHERE id = ?",
linhas
)
finally:
duracao = time.monotonic() - inicio
logging.info("job=%s operacao=atualizar_lote linhas=%d duracao=%.3f",
job_id, len(linhas), duracao)
Meça separadamente o tempo aguardando a fila e o tempo dentro da transação. Se a fila cresce, o banco não acompanha a chegada. Se a transação cresce, investigue índices, volume do lote ou trabalho indevido dentro do bloco. Se ambas estão normais e ainda há BUSY, procure outra aplicação abrindo o mesmo arquivo.
Defina alertas por tendência, não por um único pico: percentil 95 de duração, profundidade da fila, quantidade de SQLITE_BUSY e tamanho do WAL. Um painel simples transforma “às vezes trava” em uma sequência verificável. Guarde também a versão do SQLite exposta por sqlite3.sqlite_version, pois comportamento e correções podem variar entre ambientes.
Quando SQLite deixou de ser a ferramenta certa
SQLite suporta cargas consideráveis quando as transações são curtas e o desenho respeita um escritor. Mas um sistema com muitos servidores escrevendo, banco em rede, alta disponibilidade ativa ou controle central de permissões pode pedir PostgreSQL, MySQL ou outro serviço cliente-servidor.
Migrar não deve ser a primeira reação a um database is locked. Um sleep dentro de transação produzirá problemas em qualquer banco. Corrija o ciclo de vida e meça. Se a necessidade real exige múltiplos escritores distribuídos e operação contínua, a migração passa a ser uma decisão arquitetural justificada.
Checklist final
- há backup consistente antes de qualquer intervenção;
- arquivos
-wal,-shme-journalnão são apagados manualmente; - nenhuma chamada externa ocorre dentro da transação;
- conexões e cursores fecham em todos os caminhos;
- cada thread ou tarefa recebe conexão adequada;
- timeout cobre picos normais, sem esconder travamentos;
- WAL foi testado em disco local e não tratado como múltiplos escritores;
- checkpoints e tamanho do WAL são observados;
- escritas concorrentes usam fila quando necessário;
- a equipe sabe quando considerar banco cliente-servidor.
Documentação primária
- SQLite — códigos de resultado
- SQLite — transações
- SQLite — Write-Ahead Logging
- SQLite — busy timeout
- Python — módulo sqlite3
Nota de produção
Rascunho autoral orientado à busca “SQLite database is locked como resolver”. Os exemplos foram planejados para um arquivo descartável e baseados na documentação primária do SQLite e do Python. A revisão humana deve executar o laboratório em Windows, Linux ou macOS conforme o público-alvo, validar o comportamento da versão instalada e revisar os links internos antes de publicar.
Camada extra: como tomar uma decisão melhor neste cenário
Para levar SQLite database is locked: como corrigir concorrência, WAL e transações longas além de uma receita de passos, vale transformar o procedimento em um pequeno método: observar, isolar, alterar uma variável e conferir o resultado.
O erro **SQLite database is locked** aparece quando uma conexão precisa acessar o banco, mas outra mantém um bloqueio incompatível por tempo demais. A mensagem parece sugerir arquivo corrompido ou processo fantasma; na maioria dos projetos, A ideia central pode ser testada com quatro perguntas: o que foi observado, qual hipótese explica o sintoma, qual mudança é reversível e qual evidência prova que funcionou. Esse encadeamento evita que uma coincidência seja confundida com causa e torna o procedimento repetível por outra pessoa.
Integridade vem antes da aparência
Em SQLite, Python, banco de dados, WAL, o arquivo “abrir” não significa que os dados chegaram intactos. Antes de transformar, separe uma amostra com um registro normal, uma duplicidade, um valor vazio, um código com zero à esquerda e um caso de limite. Essa amostra funciona como teste de regressão: qualquer importação, limpeza ou conversão precisa preservar o que é informação e alterar somente o que é representação.
Compare contagem de linhas, quantidade de chaves únicas, somas de controle quando fizer sentido e tipos esperados. Datas, identificadores e casas decimais merecem atenção especial porque podem parecer visualmente corretos e ainda assim mudar de significado. Guarde o original sem sobrescrever e documente a regra aplicada; isso permite voltar atrás sem depender da memória.
Uma matriz simples para não misturar problemas
| Camada | Pergunta | Validação |
|---|---|---|
| Estrutura | Colunas e registros continuam presentes? | Contagens e cabeçalhos |
| Tipo | Código, data e número mantiveram o significado? | Amostra com casos-limite |
| Conteúdo | Houve perda, truncamento ou duplicação? | Comparação antes/depois |
Quando automatizar
Automação vale a pena quando a regra já é clara e o teste consegue detectar erro. Se ainda existem exceções não descritas, automatizar apenas faz o erro chegar mais rápido a mais linhas. Comece com uma amostra pequena, registre a saída esperada e só então escale para o conjunto inteiro.
Checklist de fechamento
- Registre o estado inicial antes de alterar qualquer coisa.
- Faça uma mudança por rodada e anote o efeito.
- Teste um caso normal, um caso-limite e o cenário que originalmente falhou.
- Confirme que a solução continua válida depois de reiniciar, reabrir ou repetir o fluxo.
- Guarde uma forma de voltar ao estado anterior quando a alteração for destrutiva.
Esse método acrescenta profundidade sem transformar o artigo em teoria abstrata: o leitor entende por que cada passo existe e como reconhecer quando a situação exige uma decisão diferente.
