CTO no Integrar Plus • Publicado em 16 de October de 2026
A promessa de desligar o container do Redis e concentrar toda a persistência da aplicação no PostgreSQL seduziu a comunidade Rails nos últimos meses. De um lado da discussão, defensores da simplificação operacional comemoram a chance de eliminar um serviço em memória, unificar rotinas de backup e gravar registros de domínio e jobs na mesma transação atômica. Do outro lado, engenheiros acostumados com alta concorrência sustentam que usar banco relacional como fila de mensagens continua sendo um erro conceitual, daqueles que cobram a conta em forma de bloat de tabelas, saturação de I/O e disputa agressiva por conexões.
Eu fico do lado de quem quer desligar o Redis, mas discordo categoricamente da ideia de que essa transição não tem custo. Em servidores próprios, cada processo a menos rodando significa menos alertas de uso de memória, menos configurações de persistência via dump no disco e uma superfície menor para falhas silenciosas. O problema começa quando o time trata essa mudança como uma simples troca de gem no arquivo de dependências, ignorando a forma como o motor de armazenamento do PostgreSQL reage a dados efêmeros.
Filas de segundo plano produzem um padrão de acesso hostil para bancos relacionais tradicionais. Milhares de linhas são criadas, lidas, atualizadas e destruídas em questão de segundos. No PostgreSQL, a arquitetura de controle de concorrência multiversão (MVCC) não substitui dados existentes no próprio bloco físico; ela escreve novas versões de tuplas a cada atualização e apenas marca registros antigos como mortos quando ocorre uma exclusão.
O Solid Queue foi desenhado com inteligência e usa a instrução FOR UPDATE SKIP LOCKED, o que impede que múltiplos workers fiquem travados disputando o mesmo registro na tabela de execuções prontas. Essa escolha técnica resolve o clássico gargalo de locks concorrentes que tornava bibliotecas antigas de fila em SQL inviáveis. Mesmo assim, a mecânica de travamento não blinda o banco contra o lixo deixado para trás.
Se a aplicação processa cinquenta jobs por segundo, são cinquenta inserções e cinquenta deleções contínuas nas tabelas internas da fila. Sem intervenção manual nas rotinas de manutenção, o volume de páginas de disco alocadas para índices e tabelas cresce de maneira desordenada. As consultas que buscam os próximos itens para execução começam a percorrer árvores de índices inchadas, o tempo de varredura aumenta e a memória cache do banco passa a ser consumida por blocos contendo registros obsoletos.
A configuração padrão do processo de autovacuum no PostgreSQL foi pensada para tabelas de sistemas convencionais, onde a maioria dos registros permanece gravada por meses ou anos. Por padrão, o gatilho de limpeza automática só é acionado quando cerca de vinte por cento das linhas de uma tabela sofrem alterações. Em uma tabela que acumula histórico de execuções passadas, esse percentual pode demorar horas para ser atingido. Durante esse intervalo, o bloat devora a eficiência do subsistema de disco.
Para conter esse desgaste, a solução passa por definir parâmetros agressivos e específicos para as tabelas do Solid Queue, sem alterar o comportamento global do banco de dados.
ALTER TABLE solid_queue_ready_executions SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 100,
autovacuum_vacuum_cost_limit = 2000
);
ALTER TABLE solid_queue_claimed_executions SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 100,
autovacuum_vacuum_cost_limit = 2000
);
Com valores de escala reduzidos para um por cento e limites de custo elevados, o daemon de limpeza processa essas tabelas quase em tempo real, liberando espaço interno nos blocos antes que novas páginas de armazenamento precisem ser alocadas no sistema operacional.
Outro ponto cego frequente reside no gerenciamento do pool de conexões. O servidor web Puma e o processo do Solid Queue competem pelos mesmos sockets se compartilharem cegamente a mesma configuração. Deixar ambos disputando o limite do pool principal frequentemente resulta em requisições HTTP esperando por conexões enquanto workers processam tarefas secundárias.
A separação física ou lógica da base de dados resolve essa contenção. O Rails permite configurar facilmente múltiplos bancos no mesmo cluster do PostgreSQL.
production:
primary:
adapter: postgresql
database: app_production
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
queue:
adapter: postgresql
database: app_production_queue
pool: <%= ENV.fetch("SOLID_QUEUE_THREADS", 5) %>
migrations_paths: db/queue_migrate
Criar uma base dedicada para a fila no mesmo nó do PostgreSQL mantém a facilidade operacional de gerenciar uma única instância do serviço no Linux, mas impede que picos repentinos de enfileiramento sequestrem conexões necessárias para renderizar telas no navegador. Além disso, separar a base permite definir políticas de backup distintas, já que perder o histórico de jobs concluídos raramente compromete a integridade do negócio, enquanto a base principal exige recuperação pontual rigorosa.
Existe uma contrapartida que raramente vejo ser mencionada quando discutem o abandono do Redis: a geração massiva de registros no Write-Ahead Log (WAL).
Como o Redis mantém as estruturas de fila em memória e só sincroniza periodicamente ou via append-only file em disco, o custo de I/O em rajadas de mensagens costuma passar despercebido. No PostgreSQL, cada movimentação de status de um job gera entradas obrigatórias no WAL para garantir integridade. Em topologias com replicação contínua para nós de leitura ou réplicas de contingência, o tráfego gerado pela fila viaja pela rede e consome banda de replicação.
A tabela a seguir resume as principais diferenças operacionais que observei entre os dois modelos ao gerenciar nós dedicados.
| Requisito Operacional | Redis com Sidekiq | Solid Queue no PostgreSQL |
|---|---|---|
| Dependência de processos | Processo isolado em memória | Mesma infraestrutura relacional |
| Transacionalidade com o domínio | Nula (exige padrões adicionais) | Nativa via banco de dados |
| Manutenção de tabelas | Inexistente (armazenamento volátil) | Exige autovacuum agressivo |
| Consumo de WAL e replicação | Zero no banco principal | Elevado por natureza das tuplas |
| Isolamento de falhas | Falha no Redis poupa o banco | Sobrecarga de I/O afeta o sistema |
Aposentar o Redis é uma decisão acertada para a grande maioria das aplicações que processam até algumas centenas de tarefas assíncronas por segundo. Ganhar a capacidade de enfileirar um email ou um processamento de pagamento dentro do mesmo bloco ActiveRecord::Base.transaction, com a certeza de que o job só existirá se o commit do pedido for concluído com sucesso, elimina uma classe inteira de condições de corrida.
Se o seu volume ultrapassa milhares de eventos por segundo com necessidade de latência na casa dos microssegundos para distribuição de mensagens, o PostgreSQL começará a cobrar um preço alto demais em ciclos de CPU e desgaste de NVMe. Nesses patamares, sistemas projetados para descarte rápido em memória continuam insubstituíveis. Para o restante dos cenários, a simplificação vale a pena, desde que você esteja disposto a fazer a engenharia de banco necessária em vez de acreditar em soluções que prometem gratuidade técnica.
CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.