Dev

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 = 5000 em vez de SQLITE_BUSY retries 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.