← Blog

Rode workers Sidekiq ou Celery junto com sua aplicação web

Railyard

7 de agosto de 2026 · 11 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

No momento em que uma aplicação precisa enviar um e-mail, redimensionar uma imagem ou chamar uma API de terceiros lenta como efeito colateral de uma requisição, esse trabalho precisa acontecer em algum lugar que não seja a própria requisição. O Sidekiq pra Ruby e o Celery pra Python resolvem o mesmo problema: uma fila guarda o job, e um processo worker separado puxa jobs dela e os executa, então um provedor de e-mail lento nunca faz o carregamento de uma página parecer lento.

Este guia configura um processo worker ao lado de uma aplicação web já existente: Sidekiq com Redis como backend de fila pra uma aplicação Ruby, ou Celery com Redis como broker pra uma aplicação Python, ambos executáveis com Docker Compose ou como um serviço systemd simples.

O que você vai precisar

  • Um servidor já rodando sua aplicação web, ou um novo. Um Hetzner CX23 (2 vCPU, 4 GB de RAM, 40 GB NVMe, €5,99/mês sem IVA) tem folga suficiente pra rodar um processo web, um processo worker e o Redis juntos numa aplicação pequena a média.
  • Redis. Se você ainda não tem uma instância, a imagem oficial redis:8-alpine no Compose é o jeito mais rápido de conseguir uma; o Passo 1 mostra como.
  • O código da sua aplicação já estruturado de forma que os jobs fiquem definidos num lugar que o processo worker consiga importar: um diretório app/jobs ou app/sidekiq em termos de Rails/Sidekiq, um tasks.py com decorators @app.task em termos de Celery.

Passo 1: adicionar o Redis como fila compartilhada

Tanto o Sidekiq quanto o Celery precisam de um broker pra guardar jobs entre o enfileiramento e a execução. O Redis é a escolha comum pra ambos (o Celery também suporta RabbitMQ, mas o Redis cobre a grande maioria das configurações e mantém este guia com uma única peça móvel):

services:
  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

Se essa instância de Redis é puramente uma fila de jobs e mais nada lê ou escreve nela, você não precisa das flags de ajuste de cache de uma configuração de Redis de uso geral, os padrões já bastam; só garanta que o volume esteja lá pra que jobs enfileirados mas ainda não executados sobrevivam a um restart em vez de desaparecer.

Passo 2: configurar o Sidekiq (Ruby)

Adicione a gem. Este guia usa o Sidekiq 8.1 (8.1.7 é a versão atual na RubyGems):

gem "sidekiq", "~> 8.1"
bundle install

Defina um job:

class WelcomeEmailJob
  include Sidekiq::Job

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

Aponte o Sidekiq pra instância de Redis do Passo 1 e defina uma concorrência sensata pro tamanho do servidor. Cada thread do Sidekiq mantém sua própria conexão com o banco, então a concorrência precisa combinar com o pool de conexões do seu banco, não só com a CPU que você tem de sobra:

# config/sidekiq.yml
:concurrency: 5
:queues:
  - default
  - mailers
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV.fetch("REDIS_URL") }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV.fetch("REDIS_URL") }
end

Enfileire o job de qualquer lugar da sua aplicação:

WelcomeEmailJob.perform_async(user.id)

Passo 3: configurar o Celery (Python)

Adicione a dependência. Este guia usa o Celery 5.6 (5.6.3 é a versão atual no PyPI); o extra redis instala o cliente Redis que o Celery precisa:

pip install "celery[redis]==5.6.3"

Defina a aplicação Celery e uma task em tasks.py, o layout que o próprio guia de primeiros passos do Celery usa:

import os
from celery import Celery

app = Celery("tasks", broker=os.environ["REDIS_URL"])

@app.task
def send_welcome_email(user_id):
    print(f"sending welcome email to user {user_id}")

Substitua o corpo pelo seu trabalho real. Chame a task a partir do código web com .delay(), que coloca na fila e retorna imediatamente:

from tasks import send_welcome_email

send_welcome_email.delay(user.id)

Num projeto Django, o guia “First steps with Django” do Celery coloca a aplicação em proj/celery.py e carrega as suas configurações do Django primeiro, pra que as tasks possam usar o ORM; o comando do worker então passa a ser celery -A proj worker em vez de celery -A tasks worker.

Passo 4: rodar o worker como processo próprio

Seja qual biblioteca você estiver usando, o worker é um processo separado e de longa duração, não uma thread dentro do seu servidor web. Com Docker Compose, isso significa um segundo serviço construído a partir da mesma imagem da sua aplicação web, diferindo só no comando:

services:
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    env_file: .env
    ports:
      - "3000:3000"
    depends_on:
      - redis
    restart: unless-stopped

  worker:
    build: .
    command: bundle exec sidekiq -C config/sidekiq.yml -c 5
    env_file: .env
    stop_grace_period: 30s
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

O equivalente em Celery troca os comandos de web e worker:

services:
  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    env_file: .env
    ports:
      - "8000:8000"
    depends_on:
      - redis
    restart: unless-stopped

  worker:
    build: .
    command: celery -A tasks worker --loglevel=INFO --concurrency=4
    env_file: .env
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

O stop_grace_period: 30s no worker do Sidekiq importa em todo deploy. O Docker manda TERM ao parar e mata o container 10 segundos depois por padrão, enquanto o Sidekiq espera por padrão até 25 segundos pelos jobs em execução antes de devolver os que não terminaram pro Redis. Sem o período de carência maior, o Docker mata o Sidekiq antes que ele consiga devolver esses jobs.

Nos dois casos, o arquivo .env ao lado do arquivo do Compose define REDIS_URL=redis://redis:6379/0, então o processo web e o worker apontam pro mesmo serviço Redis pelo nome dele no Compose.

Suba tudo:

docker compose up -d
docker compose ps

Você deve ver os serviços web, worker e redis todos com status Up.

Dispare um job da sua aplicação e confirme que ele rodou checando os logs do worker:

docker compose logs -f worker

Você deve ver o nome do job e uma linha de conclusão (o Sidekiq loga done: com um tempo; o Celery loga Task ... succeeded).

Passo 5: a alternativa com systemd, sem Docker

Se sua aplicação roda direto no host em vez de em containers, uma unit do systemd te dá o mesmo comportamento de “roda junto com o processo web, reinicia ao travar” sem o Compose.

# /etc/systemd/system/myapp-sidekiq.service
[Unit]
Description=Sidekiq worker for myapp
After=network.target redis-server.service

[Service]
Type=notify
WatchdogSec=10
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/usr/local/bin/bundle exec sidekiq -C config/sidekiq.yml
User=deploy
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

Type=notify com WatchdogSec segue o exemplo de unit file do próprio repositório do Sidekiq, que funciona a partir do Sidekiq 6.0.6: o Sidekiq avisa o systemd quando está pronto e continua enviando sinais de vida, e o systemd reinicia o processo em segundos se esses sinais pararem.

O Celery não envia essas notificações, então precisa da própria unit com Type=simple e sem watchdog. Uma unit notify ficaria esperando por um sinal de prontidão que nunca chega, e o systemd acabaria matando o processo:

# /etc/systemd/system/myapp-celery.service
[Unit]
Description=Celery worker for myapp
After=network.target redis-server.service

[Service]
Type=simple
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/.venv/bin/celery -A tasks worker --loglevel=INFO --concurrency=4
User=deploy
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

Habilite e inicie o que você for usar:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp-sidekiq
sudo systemctl status myapp-sidekiq

Pro Celery, use myapp-celery nos mesmos três comandos.

Você deve ver active (running), e Restart=always significa que um worker que travou volta por conta própria em vez de ficar parado silenciosamente até alguém notar que os jobs pararam de ser processados.

O caminho mais curto: fazer isso com o Railyard

Tudo nos Passos 4 e 5 (um segundo processo a partir da mesma imagem, sua própria política de restart, seu próprio escalonamento) é o que a aba Workers do Railyard configura diretamente. Ela lê seu repositório procurando por runners de job que reconhece, Sidekiq, Solid Queue, GoodJob, Resque, Celery, RQ, o queue worker do Laravel ou o Horizon, um script npm run worker, ou qualquer coisa declarada como processo no seu Procfile, Procfile.dev ou app.json, e oferece cada um como um worker que você adiciona com um clique. Cada worker roda a própria imagem da sua aplicação e escala de forma independente do seu processo web, então um pico de tráfego no site não precisa significar pagar por mais capacidade de worker também, nem o contrário.

Uma aplicação só de worker, sem nenhum processo web, também pode ser configurada pra dormir quando ociosa em Settings → General, e o Railyard a desperta na própria agenda pra esvaziar o que estiver na fila antes de deixá-la voltar a dormir, o que importa se a fila genuinamente fica quieta na maior parte do tempo e você prefere não pagar por um processo parado ocioso o dia inteiro.

Resolução de problemas

O worker inicia mas nunca pega nenhum job. Quase sempre é um REDIS_URL diferente entre o processo que enfileira os jobs e o processo que roda o worker, geralmente porque um lê de um arquivo .env e o outro de um diferente, ou um valor desatualizado embutido numa imagem no momento do build em vez de lido em tempo de execução. Confirme que os dois processos apontam pra mesma instância de Redis e pro mesmo número de banco lógico, depois confira se a fila tem algo nela (redis-cli -u $REDIS_URL llen queue:default pra nomenclatura de fila padrão do Sidekiq, ou o equivalente pra como seus nomes de fila estão configurados).

Jobs ficam na fila e nunca rodam, mesmo com o worker saudável. Confira se os nomes das filas realmente coincidem. Workers do Sidekiq só puxam das filas listadas na própria config :queues; um job enfileirado numa fila que o worker nunca foi instruído a escutar fica ali indefinidamente sem nenhum erro de nenhum dos lados. O Celery tem o mesmo modo de falha com suas routing keys se você configurou filas customizadas em vez de usar a padrão.

A memória sobe até o servidor matar o worker. Um worker com concorrência configurada mais alta do que o servidor realmente suporta é a causa mais comum; cada thread ou processo mantém sua própria memória (e, no caso do Sidekiq, sua própria conexão com o banco), então uma concorrência ajustada pra um servidor maior do que aquele em que você realmente está rodando eventualmente faz o processo ser matado pelo gerenciamento de falta de memória do kernel. Baixe a concorrência primeiro e veja se o problema volta antes de assumir que é um leak no código dos seus jobs.

O processo do worker encerra imediatamente sem erro claro. Geralmente é uma variável de ambiente que o worker precisa mas o processo web não, já que o caminho de código do worker pelas classes de job frequentemente toca em partes diferentes da configuração da sua aplicação. Rode o comando exato do worker à mão num shell com o mesmo arquivo de ambiente pra ver a exceção real em vez do que o seu gerenciador de processos resumiu.

De alguma forma duas cópias de um job rodaram. Nenhuma das duas ferramentas promete execução exatamente uma vez. Um job que lança um erro é reexecutado, e com a opção acks_late do Celery ativada, uma task interrompida no meio é entregue de novo, então um job que já tinha feito parte do trabalho pode repeti-lo. Escreva os jobs de forma que rodá-los duas vezes seja inofensivo (confira se o e-mail já foi enviado, use upserts em vez de inserts), e isso deixa de ser um problema independente de quantos workers você rodar.

Perguntas frequentes

Devo usar Sidekiq ou Celery? O que combinar com a linguagem do seu framework web. Não existe uma diferença de confiabilidade relevante que deveria levar uma aplicação Ruby pro Celery ou uma aplicação Python pro Sidekiq; os dois são maduros, os dois usam Redis por padrão, e os dois suportam retries e jobs agendados.

O Celery precisa do RabbitMQ em vez do Redis? Não. O Celery suporta os dois como broker, e o Redis é mais simples de operar se você ainda não roda RabbitMQ pra outra coisa. O RabbitMQ tem garantias de entrega mais fortes em alguns casos extremos, o que importa mais numa escala que a maioria das aplicações pequenas nem chega perto de atingir.

De quantos processos worker eu preciso? Comece com um processo numa concorrência modesta (5 threads pro Sidekiq, 4 pro Celery, como mostrado acima) e veja se os jobs acumulam na fila sob carga real antes de adicionar mais. Um segundo processo worker é mais simples de raciocinar do que aumentar a concorrência de um processo além do que a CPU do servidor e o pool de conexões do seu banco conseguem de fato suportar.

O que acontece com um job se o worker travar no meio da execução? Depende de como o worker parou. Num encerramento normal (systemctl stop, docker compose stop, um deploy), o Sidekiq espera pelos jobs em execução até o timeout de encerramento e devolve pro Redis qualquer um que não tenha terminado. Se o processo é morto abruptamente (um kill por falta de memória, kill -9, uma queda de energia), o Sidekiq open-source perde os jobs que estava executando, porque remove um job do Redis no momento em que começa a trabalhar nele; o super_fetch do Sidekiq Pro existe justamente pra fechar essa brecha. O Celery confirma uma task antes de executá-la por padrão, então uma queda abrupta perde essa task também; definir task_acks_late = True faz o Celery confirmar só depois que a task termina, então uma task interrompida é entregue de novo em vez de perdida. Se perder um job ocasional importa, mantenha os jobs idempotentes e ative o acks_late (Celery) ou registre trabalho importante em algum lugar durável antes de enfileirá-lo.

Eu preciso de uma instância de Redis separada do meu cache? Não necessariamente, mas se você compartilhar uma, mantenha a fila no próprio número de banco lógico ou prefixo de chave, e tenha cuidado com qualquer maxmemory-policy que descarte chaves sob pressão, já que uma política de eviction de cache aplicada a uma fila pode silenciosamente descartar um job que nunca foi de fato processado.

Depois que a fila e o worker estiverem falando com a mesma instância de Redis e com os mesmos nomes de fila, o resto é basicamente dimensionar a concorrência pro que o servidor e o banco conseguem de fato suportar, não chutar um número maior porque parece mais rápido.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Adicione um worker em um clique

O Railyard lê as dependências do seu repositório e oferece Sidekiq ou Celery como um worker que ele pode rodar junto do seu processo web, escalado de forma independente.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.