Buzeli
buzeliSoluções Digitais
SRE

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:

Copiar
| 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:

Copiar
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úmulo

O 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:

Copiar
# 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.service

O 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:

Copiar
#!/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.

Copiar
# /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.target

Com 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.