Programação Por Equipe DevBras

Capa do artigo: SQLite com vários processos: "database is locked" e cópia corrompida

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.

  1. 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.
  2. Configure um tempo de espera em toda conexão, para que um lock momentâneo vire espera e não erro.
  3. 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.
  4. Mantenha a transação curta. Nada de chamar API externa ou esperar o usuário com a transação aberta.
  5. 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 -wal somem 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; responde wal.
  • 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 INTO ou 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?

Avalie este artigo

Referência: https://sqliteviewer.app/