Solid Queue: entendendo fork, async e o modelo de concorrência dos workers
Um passo a passo prático sobre como o Solid Queue separa modo do supervisor do modelo de concorrência do worker, e como dimensionar threads e pool do Active Record sem chutar.

O Solid Queue virou padrão em apps Rails 8, e depois de rodar ele em produção num serviço que engolia webhooks de API, resolvi documentar o que aprendi sobre configurar workers direito. A parte que mais confunde gente não é enfileirar job: é entender como o supervisor, os processos e as threads se encaixam, e como dimensionar o pool de conexões do banco de fila sem chutar. Vou mostrar o caminho, incluindo onde me queimei.
Dois conceitos separados: supervisor e worker
Antes de mexer em config, vale separar dois conceitos que o README trata como distintos, e com razão:
- Modo do supervisor (
forkvsasync): decide se worker, dispatcher e scheduler vivem em processos forkados ou em threads do mesmo processo do supervisor. - Configuração do worker (
threads: Nvsprocesses: N): decide o tamanho do thread pool e quantos processos worker o supervisor vai forkar.
Por padrão o Solid Queue roda em modo fork: o supervisor forka um processo separado para cada worker/dispatcher/scheduler. O README é direto sobre isso: fork dá o melhor isolamento e performance, mas gasta mais memória e pode não funcionar com algumas implementações de Ruby↳Ruby3 conteúdosComo migrei meus testes automatizados de Java para Ruby… Será que fiz bem?Dev (Back & Front) · jun 2019Refatoração em RubyDev (Back & Front) · mai 20197 exemplos de linguagens de programação server-sideGestão Dev & TI · jun 2024Ver tudo em Dev (Back & Front) →. O modo async roda tudo no mesmo processo em threads diferentes. A recomendação oficial é clara: só use async se você sabe o que está fazendo e tem motivo forte. E detalhe importante: em modo async, a opção processes da configuração é ignorada.
O queue.yml na prática
A configuração vive em config/queue.yml (ou no caminho que você apontar via SOLID_QUEUE_CONFIG ou -c). Uma config de produção típica fica assim:
production:
dispatchers:
- polling_interval: 1
batch_size: 500
concurrency_maintenance_interval: 300
workers:
- queues: "*"
threads: 3
polling_interval: 2
- queues: [ real_time, background ]
threads: 5
polling_interval: 0.1
processes: 3Algumas coisas que vale fixar:
threadsconfigura o tamanho do thread pool do worker. O default éthreads: 3.processesé quantos processos worker o supervisor forka com essas configurações. O default é1. Serve para dedicar mais de um core CPU a uma fila.polling_intervalé o intervalo em segundos entre checagens por novos jobs. Default de 1s para dispatchers e 0.1s para workers.- A ordem das filas importa: com
[ real_time, background ], nada sai debackgroundenquanto houver job emreal_time.
Se você não passar configuração nenhuma, o Solid Queue sobe com um dispatcher e um worker nos defaults.
O pulo do gato: pool_size do Active Record
Aqui mora o detalhe que mais gente erra. O worker consome conexões do banco de fila para polling e heartbeat, e o thread pool usa conexões adicionais para executar os jobs. A orientação do README é explícita:
Recomenda-se definir o valor de
threadsmenor ou igual ao tamanho do connection pool do banco de fila menos 2.
Ou seja, se você configura threads: 5, o pool do banco de fila precisa de pelo menos 7 conexões. Foi meu primeiro tropeço: subi um worker com threads: 5 e um pool herdado do primário que estava apertado, e comecei a ver esperas por conexão sob carga.
# config/database.yml
production:
primary: &primary_production
<<: *default
database: app_production
queue:
<<: *primary_production
database: app_production_queue
pool: 7
migrations_paths: db/queue_migrateRodar o banco de fila separado do primário é o recomendado, mas dá para usar um banco só seguindo os passos do README (copiar o db/queue_schema.rb para uma migration normal e remover o connects_to).
Threads ou mais processos? Depende da carga
Sem torcida: a escolha entre subir threads ou processes depende do perfil dos seus jobs.
- Jobs I/O-bound (chamadas HTTP para APIs externas, esperando resposta) toleram bem um thread pool maior, porque a thread fica ociosa esperando I/O e o Ruby libera a GVL nesses momentos. Subir
threadscostuma render. - Jobs CPU-bound não ganham com mais threads no mesmo processo por causa da GVL. Aí o caminho é
processespara usar vários cores de verdade, cada processo com seu próprio thread pool.
E se você não tem motivo forte para otimizar, o default de threads: 3 já resolve a vida de muita gente. Uma coisa que aprendi apanhando: meça throughput real antes e depois de qualquer mudança. Sem número comparativo, você só trocou de config sem saber se melhorou.
Detalhe de performance nas filas
Um último ponto do README que vale internalizar: o Solid Queue foi desenhado para throughput máximo com MySQL↳MySQL8 conteúdosMySQL + Adminer + Docker Compose: montando rapidamente um ambiente para usoData · abr 2019Banco de dados MYSQL no PHPStormDev (Back & Front) · jul 2019Criando um ambiente de desenvolvimento PHP mínimo com Docker – Parte 3Dev (Back & Front) · out 2019Ver tudo em Data → 8+, MariaDB 10.6+ ou PostgreSQL 9.5+, porque esses suportam FOR UPDATE SKIP LOCKED, evitando que múltiplos workers fiquem travados esperando lock na mesma fila. Dá para usar versões mais antigas ou SQLite em apps menores, mas aí, com vários workers na mesma fila, você pode esbarrar em lock waits. Vale conferir a versão do seu banco antes de escalar horizontalmente.
Fonte: Solid Queue — README (configuração de workers e concorrência)
Este artigo foi escrito por Bisneto Braga, colunista de back-end. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
Node.js 24 chega à fase LTS: o checklist de migração pra quem tem API em produção
A versão 24 (codinome Krypton) virou Active LTS e passa a ser a escolha padrão para produção. Veja o que muda de fato no runtime e o que isso custa comparado a subir major em Rails ou Java.













