SQLite em produção: como otimizar WAL Mode, Concurrency e VFS Layers
Guia prático de otimização de SQLite em produção: WAL mode, concorrência, VFS layers e tuning para servidores de baixa latência.
Há anos que uso SQLite em projetos pessoais. Sempre que menciono isso a outros developers, a resposta é invariável: “SQLite é para desenvolvimento local, para produção usas PostgreSQL”. É um daqueles dogmas que toda a gente repete sem questionar.
O artigo da Micrologics, publicado a 29 de julho de 2026, desmonta esse dogma peça por peça. E chega numa altura em que cada vez mais projetos — incluindo o meu próprio site, que corre em SQLite — provam que a base de dados mais usada do mundo também funciona em produção.
WAL Mode: a primeira coisa a ativar
Por omissão, o SQLite usa rollback journal. Neste modo, vários leitores podem ler em simultâneo, mas um escritor bloqueia os leitores no momento do commit e os leitores bloqueiam o escritor até terminarem. Para um servidor web com 50 utilizadores concorrentes, isto é limitativo.
A solução é o Write-Ahead Logging (WAL):
PRAGMA journal_mode = WAL;
Em WAL mode, as escritas são apensadas a um ficheiro .sqlite-wal separado, em vez de modificarem o ficheiro principal. Leitores continuam a ler do ficheiro principal (e de páginas não alteradas no WAL) enquanto escritores apensam ao WAL. Leitores e escritores não se bloqueiam mutuamente.
Mas o WAL cresce. Para evitar que consuma o disco e degrade as leituras, é preciso checkpointing:
PRAGMA wal_checkpoint(PASSIVE);
O modo PASSIVE passa para o ficheiro principal o máximo de páginas possível sem bloquear ninguém. Os modos FULL, RESTART e TRUNCATE esperam por leitores e escritores para completar o checkpoint (o RESTART e o TRUNCATE reiniciam também o WAL). Para servidores com alto volume de escrita, o ideal é um checkpoint agendado em background — um cron job ou uma goroutine dedicada.
Concorrência e locking
Em modo rollback, o SQLite tem cinco estados de lock: UNLOCKED, SHARED, RESERVED, PENDING e EXCLUSIVE, e só a passagem a EXCLUSIVE (commit) exige que não haja leitores ativos. Em WAL mode o modelo é outro: vários leitores continuam a ler enquanto um único escritor acrescenta ao WAL, e só pode haver um escritor de cada vez, por isso é nos escritores que o busy_timeout faz diferença.
Para evitar contenção, o artigo recomenda:
- Manter transações curtas. Nunca fazer I/O ou chamadas de rede dentro de uma transação.
- Usar
PRAGMA busy_timeout = 5000em vez deSQLITE_BUSYretries manuais. - Em WAL mode, usar
PRAGMA synchronous = NORMAL: a base de dados não se corrompe, mas as últimas transações podem perder-se se houver uma falha de energia (o modo FULL evita isso, à custa de mais fsync). - Para máxima concorrência, usar
PRAGMA wal_autocheckpoint = 4096(ou maior) e gerir checkpoints manualmente.
VFS Layers: a arma secreta
O artigo aprofunda a arquitetura de Virtual File System (VFS) do SQLite. O VFS é a camada de abstração que permite ao SQLite funcionar em qualquer sistema de ficheiros — ou até em memória. É também o ponto onde se podem injetar otimizações radicais.
A recomendação principal: usar unix-dotfile VFS em vez do unix default se o sistema de ficheiros for NFS ou similar. O unix-dotfile usa ficheiros .lock em vez de flock(), o que evita problemas com locks de rede. Atenção: o modo WAL precisa de memória partilhada na mesma máquina e não funciona sobre sistemas de ficheiros de rede, por isso esta recomendação aplica-se ao modo rollback.
Para SSD NVMe, o artigo sugere PRAGMA mmap_size = 268435456 (256MB). Isto mapeia o ficheiro da base de dados em memória, permitindo leituras sem syscalls. Para ficheiros até 2GB, pode-se mapear a base de dados inteira.
Para servidores multi-threaded em Go ou Rust, o artigo recomenda usar SQLITE_OPEN_NOFOLLOW para evitar symlink attacks e SQLITE_OPEN_PRIVATECACHE para isolar caches entre threads.
O que eu mudei nos meus projetos
Depois de ler o artigo, fiz três mudanças imediatas em todos os meus projetos que usam SQLite em produção: ativei WAL mode, configurei busy_timeout em vez de retry loops manuais, e reduzi synchronous para NORMAL. O resultado foi uma redução de ~40% na latência p99 das escritas, sem qualquer alteração de schema ou queries.
A lição principal: SQLite em produção é uma questão de “como”, e não de “se”. Os defaults são seguros, mas são péssimos para performance. Cinco PRAGMAs bem escolhidos fazem mais diferença do que migrar para PostgreSQL.
Recursos adicionais
Partilhar
Comentários 0
Ainda sem comentários — sê o primeiro.