No meu motor de day trading, dois bots MT5 escreviam no mesmo banco SQLite, com modo WAL ligado. A lógica parecia sólida: WAL é concorrente, então múltiplos processos escrevendo ao mesmo tempo não deveria ser problema. Um dia, um dos dois processos morreu — kill forçado, sem passar pelo desligamento normal. O outro continuou rodando e, em pleno pregão, começou a reportar database is locked. Uma vez por minuto. Durante 52 minutos. O arquivo WAL, que devia ser truncado a cada checkpoint, tinha crescido para 9,4 MB — mais que o dobro do limiar padrão de 4 MB — e cada escrita nova disparava um timeout de checkpoint síncrono.

WAL garante leitor e escritor ao mesmo tempo — não sobrevivência a um kill

O write-ahead log existe pra isso: leitores e um escritor convivem sem que a leitura fique bloqueada. É uma promessa real, e ela se cumpre. O que ela não promete é que qualquer número de processos, em qualquer sistema operacional, sobrevive a qualquer forma de encerramento. Essa generalização não está em documentação nenhuma — é uma inferência que se faz sozinho, e foi ela que quebrou o bot sobrevivente.

A mecânica explica por quê. No POSIX, locks de arquivo são consultivos, via fcntl. Quando o kernel fecha os descritores de um processo morto, o lock cai junto — mesmo que o processo nunca tenha chamado um fechamento de propósito. No Windows, locks de arquivo são obrigatórios, aplicados pelo próprio sistema operacional. Um handle órfão, de um processo morto à força, pode deixar o arquivo — ou o WAL associado a ele — bloqueado para os demais escritores. Foi exatamente esse handle órfão que travou o processo sobrevivente por quase uma hora.

A intuição comum falha justo no Windows, com kill não controlado

"É só um SQLite com WAL, dá pra vários processos escreverem" funciona bem em teoria, e funciona na prática em Linux. No Windows, quando um dos processos pode morrer sem shutdown controlado — kill do gerenciador de tarefas, crash, queda de energia, restart de serviço — a mesma frase vira o roteiro do incidente. O aviso chega antes do travamento total: o tamanho do arquivo WAL passando do limiar de checkpoint (por padrão, 1.000 páginas, algo perto de 4 MB com páginas de 4 KB) já indica que os checkpoints estão falhando. Não é preciso esperar o database is locked aparecer pra agir.

LEIA TAMBÉM · Caso realUm sys.exit() num step reiniciava meu container inteiro, sem logar nadaUm programa que roda sozinho dentro de um container tinha um comando de "encerrar". Só que ele encerrava o programa todo, não a etapa. O container caía e subia de novo sem avisar ninguém.

A correção que funcionou não foi adicionar retry nem aumentar timeout. Foi isolar o banco por processo.

Isolar por processo resolve — e cobra reconciliação depois

Cada processo escritor ganhou seu próprio arquivo SQLite; nenhum arquivo ficou como alvo de escrita concorrente entre processos que podem morrer de forma independente. N processos, N arquivos, merge feito depois. O trade-off mora na mesma frase da recomendação: isolar por processo elimina o lock cruzado, mas troca "ler um banco único e consistente" por "reconciliar N arquivos depois". Não é eliminar a complexidade — é mover a complexidade da concorrência em tempo real pra um merge em lote.

Quando o travamento já aconteceu, PRAGMA wal_checkpoint(TRUNCATE) força o checkpoint e zera o WAL — é recuperação, não prevenção. No meu sistema de memória pessoal — outro projeto que roda com SQLite e WAL em produção — a configuração defensiva ficou em três pragmas: PRAGMA journal_mode=WAL, PRAGMA synchronous=FULL, PRAGMA busy_timeout=5000. Todo acesso ali fica encapsulado num bloco que sempre fecha a conexão no fim. Reduz o risco de corrupção. Não elimina a possibilidade de um kill deixar o WAL num estado ruim.

O mesmo aviso apareceu, com causa raiz diferente, num outro projeto meu: um diário de saúde pessoal rodando em Docker no Windows, com o banco num bind mount. Ali não foi kill de processo — foi o próprio bind mount corrompendo a persistência aos poucos. Cerca de 75% dos registros se perderam em 25 dias; a solução trocou o agendamento por Task Scheduler. Mecanismo diferente, mesmo aviso de fundo: SQLite, WAL e Windows exigem uma atenção que Linux dispensa.

LEIA TAMBÉM · Caso realTrês falhas silenciosas depois, meu bot de Telegram reinicia sozinho a cada 12 horasUm bot pode continuar aberto e respondendo enquanto uma parte dele já parou de funcionar. Um supervisor é um trecho de código que reinicia o bot de tempos em tempos, sozinho, para que essa falha silenciosa não dure.

WAL resolve concorrência entre leitores e um escritor educado — um processo que fecha a conexão do jeito certo. Não resolve escritor morto à força no meio de uma transação, no Windows. A regra prática: se o processo pode morrer sem passar pelo shutdown, não compartilhe o mesmo arquivo SQLite entre escritores. Isole por processo, reconcilie depois. Um único processo escritor, com desligamento controlado, carrega um risco bem menor — mas essa já é outra classificação de problema.