--:--:--

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,

Duas conexões lendo e apenas uma conexão escrevendo no arquivo SQLite
Imagem de apoio ao tema do artigo.

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.

Duas conexões lendo e apenas uma conexão escrevendo no arquivo SQLite 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.

Fluxo do modo WAL com escrita no log, leitores no banco e checkpoint 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, -shm e -journal nã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

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

CamadaPerguntaValidação
EstruturaColunas e registros continuam presentes?Contagens e cabeçalhos
TipoCódigo, data e número mantiveram o significado?Amostra com casos-limite
ConteúdoHouve 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.

#SQLite #Python #banco de dados #WAL #concorrência