Fila Resque com 1.480 jobs travados: como diagnosticar acúmulo quando o worker está vivo e o Redis está saudável
Publicado em 29 de abril de 2026
O alerta que não veio
Um cliente fintech reportou que a fila de rastreio de entregas estava acumulando. O print mostrava o quadro:
| Fila | Jobs | Status |
|------------|-------|--------------|
| postbacks | 0 | Vazia |
| payments | 0 | Vazia |
| test_queue | 0 | Vazia |
| tracking | 1480 | Acumulando |
| default | 18 | Acumulando |
| vendas | 7 | Processando |Primeira verificação: o processo worker estava vivo nas duas instâncias do ASG. O serviço systemd reportava 'active (running)'. O Redis (ElastiCache) respondia normalmente. Nenhum erro nos logs. Do ponto de vista dos monitores, tudo estava verde.
Fila acumulando com worker vivo e Redis saudável é uma das situações mais traiçoeiras em sistemas de filas. Não há alarme piscando, nenhum processo morto, nenhum stack trace. A única evidência é o número de jobs crescendo silenciosamente.
O diagnóstico: matemática simples revelou o problema estrutural
O worker estava configurado para processar 6 filas em um único processo sequencial. A fila 'tracking' dispara jobs que fazem uma requisição HTTP para a API dos postal tracking por venda — uma chamada de rede bloqueante, sem paralelismo.
A conta era direta:
Disparo (EventBridge): a cada 15 minutos
Jobs enfileirados por ciclo: ~1.480
Workers disponíveis: 2 (1 por instância ASG, baked na AMI)
Tempo por job: ~1.5s (1 req HTTP para API externa)
Tempo para drenar 1.480 jobs com 2 workers:
1.480 / 2 / 1 job por vez = 740 jobs por worker
740 × 1.5s = ~18 minutos
Próximo disparo chega em: 15 minutos
Déficit por ciclo: +3 minutos de acúmuloO worker nunca conseguia terminar um ciclo antes do próximo começar. A fila não estava travada — estava acumulando de forma estrutural, um ciclo de 15 minutos atrás do outro.
Por que as outras filas também sofriam
O worker single-thread processava as 6 filas em ordem sequencial: postbacks → payments → test_queue → tracking → default → vendas. Quando chegava na fila 'tracking' com 1.480 jobs, o worker ficava preso ali por ~18 minutos fazendo chamadas HTTP bloqueantes.
Durante esse tempo, novos jobs nas filas 'default' e 'vendas' ficavam esperando. Não havia starvation crítica neste caso porque as outras filas tinham volume baixo, mas o padrão é perigoso: uma fila lenta com I/O bloqueante segura todas as filas que vêm depois dela na sequência.
Esse é o head-of-line blocking em sistemas de filas: o job mais lento na frente da fila impede o processamento de tudo que está atrás, mesmo que os jobs seguintes sejam rápidos.
Como identificar o padrão: comandos de diagnóstico
Para chegar a esse diagnóstico, o caminho foi verificar estado dos workers, profundidade das filas e logs em sequência:
# 1. Estado dos workers registrados no Redis
redis-cli -h <redis-endpoint> smembers resque:workers
# Retorno: hostname:PID:fila1,fila2,...,filaN
# Um único worker listando todas as filas = single-thread
# 2. Profundidade de cada fila
redis-cli -h <redis-endpoint> llen resque:queue:tracking
redis-cli -h <redis-endpoint> llen resque:queue:default
redis-cli -h <redis-endpoint> llen resque:queue:vendas
# 3. Jobs com falha
redis-cli -h <redis-endpoint> llen resque:failed
redis-cli -h <redis-endpoint> lrange resque:failed 0 -1
# 4. Processo worker no host
ps aux | grep resque | grep -v grep
sudo systemctl status app-worker.serviceO ponto-chave: um worker registrado como 'hostname:PID:postbacks,payments,test_queue,tracking,default,vendas' está processando todas essas filas em sequência, em uma única thread. Se qualquer fila tiver jobs lentos, todas as outras esperam.
O gap de observabilidade: sem alarme de profundidade de fila
O maior problema operacional não era o acúmulo em si — era não saber que estava acumulando. O ambiente tinha alarmes de CPU, memória e erros HTTP, mas nenhum alarme de profundidade de fila no CloudWatch.
Para publicar métricas de fila no CloudWatch via script de coleta periódico:
#!/usr/bin/env python3
import boto3
import redis
import time
r = redis.Redis(host='<redis-endpoint>', port=6379)
cw = boto3.client('cloudwatch', region_name='us-east-1')
queues = ['tracking', 'default', 'vendas', 'postbacks']
for queue in queues:
depth = r.llen(f'resque:queue:{queue}')
cw.put_metric_data(
Namespace='App/Queues',
MetricData=[{
'MetricName': 'QueueDepth',
'Dimensions': [{'Name': 'QueueName', 'Value': queue}],
'Value': depth,
'Unit': 'Count',
'Timestamp': time.time()
}]
)
print(f'{queue}: {depth} jobs')Com as métricas no CloudWatch, o alarme é trivial: profundidade > 500 jobs na fila 'tracking' por mais de 10 minutos → alerta. Sem isso, o acúmulo só é descoberto quando alguém olha manualmente o painel.
As soluções: do paliativo ao correto
Opção 1 — Worker dedicado por fila lenta (sem mudança de código)
A solução mais rápida para produção: criar um serviço systemd separado para a fila lenta, com a variável de ambiente QUEUE configurada para processar apenas aquela fila.
# /etc/systemd/system/app-worker-tracking.service
[Unit]
Description=App Queue Worker — postal tracking Only
After=docker.service
Requires=docker.service
[Service]
Type=simple
Restart=always
RestartSec=10
ExecStart=/usr/bin/docker exec myapp-php php app/Workers/resque-worker.php
Environment="QUEUE=tracking"
Environment="REDIS_HOST=<redis-endpoint>"
Environment="REDIS_PORT=6379"
[Install]
WantedBy=multi-user.targetCom 2 instâncias no ASG e 1 worker dedicado por instância: 1.480 / 2 workers / 1.5s = ~11 minutos. Ainda próximo do limite de 15 minutos, mas resolve o problema de bloqueio das outras filas. Para folga maior, aumentar o mínimo do ASG de 2 para 3 instâncias.
Opção 2 — Paralelismo no job (mudança de código)
A solução correta a médio prazo: agrupar as vendas em lotes ao enfileirar e usar curl_multi_exec (PHP) ou asyncio/aiohttp (Python) para disparar N requisições em paralelo dentro de um único job.
Com lotes de 10 e paralelismo real: 148 jobs ao invés de 1.480, cada job fazendo 10 requisições em paralelo. Tempo total com 2 workers: menos de 2 minutos para um ciclo completo.
O padrão se aplica além do Resque
Este diagnóstico vale para qualquer sistema de filas onde um worker processa múltiplas filas em sequência com I/O bloqueante:
Sidekiq (Ruby): worker com múltiplas queues na mesma thread — fila com jobs lentos de API externa segura todas as outras.
BullMQ (Node.js): worker sem concurrência configurada (padrão 1) em fila com await de chamadas HTTP — mesmo problema, diferente runtime.
AWS SQS + Lambda: sem esse problema por design (Lambda escala horizontalmente por default), mas filas SQS processadas por EC2 single-thread replicam o padrão.
A regra é simples: se o seu worker faz chamada de rede externa e processa múltiplas filas na mesma thread, qualquer fila com API lenta vai segurar todas as outras. O monitoramento de profundidade de fila não é opcional — é o único sinal que chega antes do acúmulo virar incidente.
Lição operacional
Três mudanças de configuração que eliminam esse classe de problema:
1. Worker dedicado por fila com I/O externo lento. Nunca misturar filas de API externa bloqueante com filas de processamento rápido no mesmo worker.
2. Alarme de profundidade de fila no CloudWatch. Threshold: qualquer fila principal com profundidade > N por mais de 1 ciclo de disparo → alerta imediato.
3. ASG mínimo compatível com o volume de jobs. Se cada instância tem 1 worker e o ciclo de jobs exige N workers para drenar dentro do intervalo de disparo, o mínimo do ASG precisa ser N.
