Modelo de confiabilidade
Signal Client combina rotação de webhooks com persistência opcional de fallback para que tráfego em pico e falhas temporárias não se transformem em atualizações perdidas no lado do bot. O desenho é intencionalmente em camadas: primeiro tenta a entrega ao vivo pelo pool, depois enfileira envelopes que falharam e faz novas tentativas com backoff exponencial. Isso dá às equipes de bot um caminho prático para confiabilidade sem obrigar cada serviço produtor a implementar seu próprio scheduler de retry.
O FallbackWorker é a primitiva central de confiabilidade. Quando o sistema de fallback está habilitado e uma chamada de send falha, o worker persiste o envelope em um FallbackStore e tenta novamente em lotes executados em background. Depois de esgotar o número máximo de tentativas, o envelope é movido para uma partição de dead-letter para inspeção manual.
Referência de configuração de fallback
O objeto FallbackConfig controla todos os aspectos do sistema de retry. Todos os campos, exceto enabled e store, possuem padrões sensatos, então uma configuração mínima só precisa ativar a feature e escolher um backend de persistência.
| Campo | Tipo | Padrão | Finalidade |
| enabled | boolean | obrigatório | Ativa o sistema de fallback. Quando verdadeiro, o worker começa no construtor e inicia o processamento imediatamente. |
| store | file | redis | obrigatório | Backend de persistência. file usa um arquivo JSON local; redis usa chaves compartilhadas com o prefixo spraxium:signal:fallback:. |
| retryIntervalMs | number | 5000 | Intervalo base entre ciclos de processamento de lotes para retry. Também serve como base da fórmula de backoff exponencial. |
| maxAttempts | number | 10 | Número máximo de tentativas antes de mover um envelope para dead-letter. Depois disso, a recuperação exige intervenção manual. |
| batchSize | number | 20 | Quantidade máxima de envelopes reivindicados por ciclo de processamento. Lotes maiores aumentam throughput, mas também a pressão em picos. |
| events | string[] | undefined | Filtro opcional de nomes de evento. Quando definido, apenas eventos dessa lista entram na fila ao falhar. Todos os demais continuam falhando pelo fluxo normal. |
| filePath | string | .spraxium-signal-fallback.json | Caminho para o arquivo JSON de fallback. Só é usado quando store é file. O diretório é criado automaticamente se não existir. |
| redisUrl | string | undefined | URL de conexão do Redis. Está aqui apenas para referência documental. Você deve fornecer uma instância de RedisFallbackStore como segundo argumento do construtor. |
Escolhendo um fallback store
A interface FallbackStore define um contrato de persistência com oito métodos: enqueue, claimBatch, markProcessed, reschedule, moveToDeadLetter, has, pendingCount e deadLetterCount. Duas implementações embutidas cobrem os cenários de implantação mais comuns. Você também pode implementar a interface por conta própria para backends customizados.
import { SignalClient } from '@spraxium/signal-client';
const client = new SignalClient({
webhookUrls: [process.env.SIGNAL_WEBHOOK_URL_1!],
hmacSecret: process.env.SIGNAL_HMAC_SECRET!,
rateLimitStrategy: 'wait',
fallback: {
enabled: true,
store: 'file',
filePath: '.spraxium-signal-fallback.json',
retryIntervalMs: 5_000,
maxAttempts: 10,
batchSize: 20,
events: ['config.update', 'billing.status.changed'],
},
});FileFallbackStore mantém entradas pendentes, dead-letter e processadas em um único arquivo JSON. Ele carrega o arquivo sob demanda no primeiro acesso e grava novamente após cada mutação. A lista de entradas processadas é automaticamente reduzida para as últimas 5000 quando ultrapassa 10000, evitando crescimento sem limite. Esse store funciona bem em desenvolvimento local e em serviços pequenos com uma única instância.
RedisFallbackStore persiste entradas como chaves individuais no Redis sob o prefixo spraxium:signal:fallback:pending:, com dead-letters sob spraxium:signal:fallback:deadletter: e IDs processados em um conjunto em spraxium:signal:fallback:processed. O Redis é preferível para emissores com múltiplas instâncias, porque o estado de fallback é compartilhado entre réplicas sem coordenação pelo sistema de arquivos.
Backoff exponencial e jitter
Quando uma tentativa de retry falha, o worker reagenda o envelope usando uma fórmula de backoff exponencial com jitter aditivo. Essa fórmula evita tempestades de retry, espalhando as tentativas por intervalos cada vez maiores, enquanto o componente de jitter impede que vários envelopes tentem de novo exatamente no mesmo instante.
A fórmula exata é: retryIntervalMs * 2^min(attempt, 8) + random() * retryIntervalMs * 0.3. O expoente é limitado a 8 para evitar atrasos absurdamente longos em contagens altas de tentativa. Com retryIntervalMs padrão de 5000 e maxAttempts de 10, o backoff máximo antes do jitter fica em cerca de 1280 segundos, aproximadamente 21 minutos, na oitava tentativa.
| Tentativa | Atraso base | Faixa de jitter | Faixa efetiva |
| 1 | 10 s | 0 a 1,5 s | 10 a 11,5 s |
| 2 | 20 s | 0 a 1,5 s | 20 a 21,5 s |
| 3 | 40 s | 0 a 1,5 s | 40 a 41,5 s |
| 5 | 160 s | 0 a 1,5 s | 160 a 161,5 s |
| 8+ | 1280 s | 0 a 1,5 s | 1280 a 1281,5 s, no teto |
Tratamento de dead-letter
Quando um envelope esgota o maxAttempts configurado, o worker o move para a partição de dead-letter via moveToDeadLetter. A entrada original é removida da fila pendente e armazenada com o último motivo de erro anexado. Envelopes em dead-letter nunca recebem retry automático, então exigem investigação manual.
No store em arquivo, entradas de dead-letter se acumulam no array deadLetter do arquivo JSON. No store em Redis, elas viram chaves individuais sob spraxium:signal:fallback:deadletter:. Ambos os stores acompanham essa quantidade via deadLetterCount, que você deve monitorar como parte das suas verificações operacionais de saúde.
O método stats do FallbackWorker retorna as contagens de pending e deadLetter em uma única chamada. Expor esses dados ao seu sistema de monitoramento é a forma mais simples de detectar falhas persistentes de entrega antes que elas afetem o comportamento visível do bot.
const { pending, deadLetter } = await client.fallback!.stats();
if (deadLetter > 0) {
logger.warn(
`Signal fallback: ${deadLetter} dead-letter envelopes require investigation`,
);
}
if (pending > 100) {
logger.warn(
`Signal fallback: ${pending} pending envelopes, possible sustained delivery failure`,
);
}Ciclo de vida de uma entrada de fallback
Enfileiramento do envelope em caso de falha
Quando uma chamada de send lança erro e o fallback está habilitado, o worker verifica se o evento corresponde ao filtro events ou, se não houver filtro, considera todos os eventos elegíveis. Em seguida, ele faz deduplicação por nonce para evitar enfileiramento em duplicidade e cria uma FallbackEntry com o envelope, nome do evento, guild ID, zero tentativas e um nextAttemptAt ajustado para agora mais retryIntervalMs.
Claim em lote e retry
A cada ciclo de processamento, a cada retryIntervalMs, o worker chama
claimBatch para puxar até batchSize entradas cujo nextAttemptAt já
passou. Para cada entrada, ele chama send no client com skipFallback: true para evitar loops de reenfileiramento. Se o envio der certo, a
entrada é marcada como processada.
Reagendamento exponencial em caso de falha
Quando uma tentativa de retry falha, o worker calcula o próximo instante de tentativa usando a fórmula de backoff exponencial com jitter. A contagem de tentativas da entrada é incrementada, o motivo do erro é gravado em lastError e a entrada é reagendada via reschedule. Isso continua até a entrega funcionar ou o limite de tentativas ser atingido.
Dead-letter ao esgotar
Quando a contagem de tentativas atinge maxAttempts, o worker chama moveToDeadLetter com o motivo final do erro. A entrada é removida em definitivo da fila pendente e colocada na partição de dead-letter. A partir daí, só intervenção manual ou um script customizado pode reprocessá-la.
Perfis de confiabilidade por cenário
| Cenário | Perfil recomendado | Trade-off |
| Eventos de baixo volume com tolerância a recuperação manual | estratégia skip, fallback desabilitado | Menor complexidade possível. O produtor precisa assumir a lógica de retry ou aceitar perdas ocasionais. |
Picos moderados em que throttling temporário é esperado | estratégia | Absorve automaticamente picos curtos de rate limit. A latência fica um pouco maior sob pressão. |
| Eventos críticos de negócio com baixa tolerância a perda | estratégia wait, fallback habilitado com store em arquivo | Retry persistente com monitoramento de dead-letter. Bom para implantações com instância única. |
Serviços com múltiplas instâncias e necessidade de estado compartilhado | estratégia | Estado de fallback compartilhado entre réplicas. Exige infraestrutura Redis. |
Eventos mistos de alto throughput com confiabilidade seletiva | fallback habilitado com filtro events apenas para nomes críticos | Concentra recursos de retry nos eventos importantes. Eventos não críticos falham rápido, sem sobrecarga de fila. |
Desligamento gracioso
Chame destroy no client durante a sequência de encerramento da sua aplicação para parar o FallbackWorker de forma limpa. Isso limpa o timer interno de setInterval e evita loops parciais de retry durante rolling deployments. O worker usa timer.unref() para não manter o processo Node.js vivo, mas destruir explicitamente o client garante uma limpeza determinística.
process.on('SIGTERM', () => {
client.destroy();
process.exit(0);
});Se você pular a chamada de destroy, ciclos pendentes de retry podem ser executados parcialmente durante o shutdown, com potencial de produzir entregas em duplicidade. Em ambientes conteinerizados, onde desligamentos acontecem com frequência, isso é ainda mais importante.
Runbook operacional
Observe o comportamento do pool continuamente
Acompanhe o status do pool com client.pool.status() para entender quais
endpoints de webhook estão saudáveis, limitados ou indisponíveis.
Correlacione padrões de rate limit com a carga dos webhooks no Discord para
decidir se você precisa adicionar mais webhooks ou reduzir a frequência de
envio.
Monitore o crescimento da fila de fallback
Quando o fallback está habilitado, contagens crescentes de pending são o primeiro sinal de falhas sustentadas de transporte. Exponha a saída do método stats ao seu sistema de monitoramento e configure alertas com limiares coerentes com o volume dos seus eventos. Quando a contagem de pending permanece acima de zero por vários minutos, normalmente existe um problema sistêmico.
Investigue acúmulo de dead-letter
Crescimento de dead-letter deve disparar investigação imediata. Verifique o
lastError armazenado em cada entrada de dead-letter para entender se as
falhas são transitórias, como rate limits e erros de rede, ou permanentes,
como webhook inválido ou segredo alterado. No store em arquivo, inspecione
diretamente o array deadLetter no JSON. No Redis, faça scan das chaves
sob spraxium:signal:fallback:deadletter:*.
Ajuste janelas de retry pela criticidade
Eventos de alto impacto, como billing ou escalonamento de moderação, justificam maxAttempts maiores e retryIntervalMs menores para novas tentativas iniciais mais rápidas. Eventos de baixo impacto devem usar ajustes mais leves para evitar que a pressão da fila engula o tráfego crítico. Use o filtro events para aplicar fallback apenas aos nomes de evento que realmente precisam disso.
Destrua emissores durante o shutdown
Sempre chame client.destroy() no handler de SIGTERM ou no fluxo de
shutdown gracioso. Isso interrompe o timer do fallback worker e evita
processamento em duplicidade durante rolling deployments. Em ambientes
serverless, nos quais o processo pode ser congelado em vez de encerrado,
considere persistir o estado pendente antes de retornar do handler.