<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rupelio</title>
    <description>The latest articles on DEV Community by Rupelio (@rupelio).</description>
    <link>https://dev.to/rupelio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023096%2F6d7a81c5-12a1-4a39-8ba5-3d6ca9c1b1cf.jpg</url>
      <title>DEV Community: Rupelio</title>
      <link>https://dev.to/rupelio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rupelio"/>
    <language>en</language>
    <item>
      <title>A engenharia de nunca perder um webhook: fila, retry e idempotência</title>
      <dc:creator>Rupelio</dc:creator>
      <pubDate>Thu, 09 Jul 2026 18:50:14 +0000</pubDate>
      <link>https://dev.to/rupelio/a-engenharia-de-nunca-perder-um-webhook-fila-retry-e-idempotencia-3j55</link>
      <guid>https://dev.to/rupelio/a-engenharia-de-nunca-perder-um-webhook-fila-retry-e-idempotencia-3j55</guid>
      <description>&lt;h1&gt;
  
  
  Por que entregar um webhook de forma confiável é mais difícil do que parece
&lt;/h1&gt;

&lt;p&gt;Todo mundo que já integrou um gateway de pagamento (Kiwify, Hotmart, Stripe, Asaas) conhece o roteiro: a plataforma manda um &lt;code&gt;POST&lt;/code&gt; pro seu endpoint avisando "pedido aprovado", e o seu sistema libera o acesso do cliente. Simples. Até o dia em que um cliente paga e não recebe nada, e você descobre que aquele webhook se perdeu no meio do caminho.&lt;/p&gt;

&lt;p&gt;O que parece um &lt;code&gt;POST&lt;/code&gt; trivial esconde um problema clássico de sistemas distribuídos: &lt;strong&gt;entrega confiável entre dois sistemas que falham em momentos diferentes.&lt;/strong&gt; Este artigo destrincha os pontos que tornam isso difícil e como se resolve cada um. No fim, comento a ferramenta que estou construindo em cima dessas ideias.&lt;/p&gt;

&lt;h2&gt;
  
  
  O ingênuo: receber e processar
&lt;/h2&gt;

&lt;p&gt;A primeira versão que todo mundo escreve é assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /webhook -&amp;gt; valida -&amp;gt; libera acesso -&amp;gt; envia e-mail -&amp;gt; responde 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O problema aparece rápido. Se "libera acesso" e "envia e-mail" demoram, ou dependem de uma API externa lenta, a resposta &lt;code&gt;200&lt;/code&gt; demora junto. E o remetente (o gateway) não espera pra sempre: ele tem um timeout. Se você não confirma a tempo, ele considera falha, mesmo que seu processamento termine depois. Resultado: do lado dele, "falhou"; do seu lado, pode ter processado, ou não. Ambiguidade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ponto 1: responder rápido, processar depois
&lt;/h2&gt;

&lt;p&gt;A primeira correção é desacoplar recepção de processamento:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /webhook -&amp;gt; grava o evento cru -&amp;gt; responde 200 imediatamente
(em segundo plano) worker -&amp;gt; lê o evento -&amp;gt; processa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O endpoint agora faz só duas coisas: persistir o payload e responder. Milissegundos. O trabalho pesado sai da frente e roda num worker separado, no seu ritmo. O gateway recebe o &lt;code&gt;2xx&lt;/code&gt; rápido e fica feliz.&lt;/p&gt;

&lt;p&gt;Isso introduz a necessidade de uma fila (ou de uma tabela de eventos funcionando como fila). E fila traz os próximos problemas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ponto 2: idempotência (o cliente não pode receber o produto duas vezes)
&lt;/h2&gt;

&lt;p&gt;Gateways reenviam webhooks quando acham que falharam. Isso é bom (não perde evento), mas significa que &lt;strong&gt;você vai receber o mesmo evento mais de uma vez.&lt;/strong&gt; Se o seu worker processa cada entrega cegamente, o cliente recebe o produto duas vezes, ou toma dois e-mails, ou você conta a venda em dobro.&lt;/p&gt;

&lt;p&gt;A solução é idempotência: cada evento precisa de uma chave estável, e você processa cada chave uma única vez. Um detalhe que pega muita gente: use um identificador do &lt;strong&gt;recurso&lt;/strong&gt; (o id do pedido/venda), não um id da tentativa de entrega. Na Kiwify, por exemplo, o id do "envelope" muda a cada reenvio, então ele não serve pra deduplicar. No banco, um &lt;code&gt;UNIQUE&lt;/code&gt; na chave de idempotência resolve de forma limpa: a segunda inserção do mesmo evento simplesmente não entra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ponto 3: retry com backoff (o servidor de destino também cai)
&lt;/h2&gt;

&lt;p&gt;Até aqui protegemos contra o nosso endpoint ser lento. Mas o worker, ao entregar o evento pro destino final (o sistema do cliente), enfrenta o mesmo problema ao contrário: e se o destino estiver fora do ar?&lt;/p&gt;

&lt;p&gt;Reentregar na hora, em loop, é a pior ideia: você martela um servidor que já está sofrendo. A resposta é &lt;strong&gt;backoff exponencial&lt;/strong&gt;: falhou, espera um pouco e tenta de novo, aumentando o intervalo a cada tentativa (1min, 5min, 15min, 1h). Isso dá tempo do destino se recuperar sem ser bombardeado. E, depois de N tentativas sem sucesso, o evento vai pra uma "fila morta" (Dead Letter Queue) pra inspeção manual, em vez de tentar pra sempre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ponto 4: visibilidade e replay
&lt;/h2&gt;

&lt;p&gt;Quando algo dá errado (e vai), você precisa enxergar. Sem log do payload que entrou, do que saiu e da resposta exata do destino, você fica no escuro. Guardar isso permite duas coisas valiosas: diagnosticar ("o servidor devolveu 500 nesse header") e &lt;strong&gt;reprocessar em massa&lt;/strong&gt; (o destino ficou 2h fora, você reenvia todos os eventos daquele período de uma vez, em vez de um por um).&lt;/p&gt;

&lt;h2&gt;
  
  
  Ponto 5: aguentar o pico
&lt;/h2&gt;

&lt;p&gt;Tudo isso é testado no pior momento: a Black Friday ou o lançamento, quando chegam milhares de eventos em minutos. O endpoint de ingestão precisa ser leve o suficiente pra absorver o pico sem cair (por isso ele só persiste e responde), e a fila precisa segurar o acúmulo enquanto o worker drena no seu ritmo. Desacoplar recepção de processamento não é elegância acadêmica: é o que mantém tudo de pé quando a carga sobe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Juntando as peças
&lt;/h2&gt;

&lt;p&gt;Uma arquitetura confiável de webhook, no mínimo, tem: um endpoint de ingestão que persiste e responde &lt;code&gt;2xx&lt;/code&gt; na hora; uma fila entre recepção e processamento; idempotência por chave estável; retry com backoff exponencial e uma DLQ; e logs que permitam diagnóstico e replay. Nenhuma dessas peças é exótica sozinha. A dificuldade é que você precisa de &lt;strong&gt;todas juntas&lt;/strong&gt;, corretas, pra não perder um evento, e mantê-las é trabalho contínuo.&lt;/p&gt;

&lt;p&gt;Foi partindo disso que comecei a construir o &lt;a href="https://hooksafe.com.br" rel="noopener noreferrer"&gt;HookSafe&lt;/a&gt;: uma camada que fica entre o gateway e o seu servidor, responde &lt;code&gt;2xx&lt;/code&gt; na hora, e cuida de fila, retry, idempotência e replay pra você, sem precisar reescrever o seu sistema. Está em early access. Se o tema te interessa, o problema é mais fundo do que parece, e é um puta exercício de engenharia.&lt;/p&gt;

&lt;p&gt;Como você resolve entrega confiável de webhook nos seus projetos? Curioso pra saber as abordagens de vocês nos comentários.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Três bugs que cometi construindo um sistema de confiabilidade (e os três fingiram que deu tudo certo)</title>
      <dc:creator>Rupelio</dc:creator>
      <pubDate>Thu, 09 Jul 2026 18:44:35 +0000</pubDate>
      <link>https://dev.to/rupelio/tres-bugs-que-cometi-construindo-um-sistema-de-confiabilidade-e-os-tres-fingiram-que-deu-tudo-5cl2</link>
      <guid>https://dev.to/rupelio/tres-bugs-que-cometi-construindo-um-sistema-de-confiabilidade-e-os-tres-fingiram-que-deu-tudo-5cl2</guid>
      <description>&lt;p&gt;Passei os últimos dias construindo o HookSafe, uma camada que fica entre a plataforma de pagamento e o servidor do cliente para garantir que nenhum webhook se perca. A promessa do produto é uma só: se o seu servidor cair, eu seguro o evento e insisto até entregar.&lt;/p&gt;

&lt;p&gt;Cometi três bugs no caminho. O que me fez escrever este texto não foi a burrice de cada um, foi perceber, depois, que os três tinham a mesma forma: todos faziam uma falha parecer um sucesso. Num sistema cujo produto é confiabilidade, é difícil imaginar categoria de bug mais cruel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: engoli o erro, e o sistema jurou que tinha entregue
&lt;/h2&gt;

&lt;p&gt;A função que entrega o evento no servidor do cliente ficou assim:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go&lt;br&gt;
resposta, err := clienteHTTP.Do(requisicao)&lt;br&gt;
if err != nil {&lt;br&gt;
    return "", nil   // &amp;lt;- olhe com carinho&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Eu quis escrever &lt;code&gt;return "", err&lt;/code&gt;. Escrevi &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;O efeito: apontei o destino para uma porta onde não havia nada escutando. O &lt;code&gt;Do&lt;/code&gt; devolveu um belo &lt;code&gt;connection refused&lt;/code&gt;. E a minha função respondeu ao worker: "sem erro, chefe". O worker, obediente, marcou o evento como &lt;strong&gt;entregue&lt;/strong&gt;, com o status da resposta vazio, e seguiu a vida.&lt;/p&gt;

&lt;p&gt;No banco:&lt;br&gt;
&lt;code&gt;id | pedido_id |  status  | tentativas | resposta&lt;br&gt;
----+-----------+----------+------------+----------&lt;br&gt;
  6 | 9002      | entregue |          0 |&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Um evento que nunca saiu do lugar, registrado como entregue. Se isso estivesse em produção, um cliente teria pagado, não receberia nada, e o meu painel mostraria, orgulhoso, que a entrega foi um sucesso.&lt;/p&gt;

&lt;p&gt;Aquele &lt;code&gt;if err != nil { return err }&lt;/code&gt; que a gente reclama de repetir em Go existe exatamente por isso. A linguagem te obriga a decidir o que fazer com a falha, toda vez. O preço da verbosidade é que ninguém engole um erro sem querer... a menos que digite &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: o log mentiu
&lt;/h2&gt;

&lt;p&gt;Corrigi o primeiro bug, rodei de novo, e o worker começou a cuspir isto, a cada cinco segundos, para sempre:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;worker: erro ao marcar morto 7: ERROR: column "reposta" does not exist&lt;br&gt;
worker: evento 7 esgotou as tentativas, marcado como MORTO&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Leia as duas linhas de novo. A primeira diz que falhou ao gravar. A segunda anuncia que gravou.&lt;/p&gt;

&lt;p&gt;O código:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go&lt;br&gt;
if erroBanco := repo.MarkDead(ctx, evento.ID, descricao); erroBanco != nil {&lt;br&gt;
    log.Printf("erro ao marcar morto %d: %v", evento.ID, erroBanco)&lt;br&gt;
}&lt;br&gt;
log.Printf("evento %d esgotou as tentativas, marcado como MORTO", evento.ID)&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Faltou um &lt;code&gt;continue&lt;/code&gt;. O log de sucesso rodava sempre, tivesse a operação funcionado ou não.&lt;/p&gt;

&lt;p&gt;O bug original era bobo: escrevi &lt;code&gt;reposta&lt;/code&gt; em vez de &lt;code&gt;resposta&lt;/code&gt; num &lt;code&gt;UPDATE&lt;/code&gt;. Mas o reposta só me custou um minuto depois que eu vi a mensagem de erro. O que quase me custou uma hora foi o log seguinte, afirmando com todas as letras que o evento tinha sido marcado como morto. Eu fui olhar o banco procurando entender por que um evento "morto" continuava sendo processado, quando a resposta estava na linha de cima da minha própria tela.&lt;/p&gt;

&lt;p&gt;Um log que mente é pior que a ausência de log. Sem log você sabe que não sabe. Com um log mentiroso, você depura com confiança na direção errada.&lt;/p&gt;

&lt;p&gt;E o efeito colateral era divertido: como o &lt;code&gt;MarkDead&lt;/code&gt; falhava, o evento nunca saía do status &lt;code&gt;falhou&lt;/code&gt;, o &lt;code&gt;next_retry_at&lt;/code&gt; já tinha vencido, e o worker o pegava de novo, e de novo, e de novo. Um loop infinito construído a partir de um "s" faltando.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: cinco nanossegundos
&lt;/h2&gt;

&lt;p&gt;O worker deveria acordar a cada cinco segundos. Configurei assim:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go&lt;br&gt;
WorkerInterval: lerDuracao("WORKER_INTERVAL", 5),&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
E o log saiu assim:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;23:00:09 worker: procurando eventos pendentes...&lt;br&gt;
23:00:09 worker: procurando eventos pendentes...&lt;br&gt;
23:00:09 worker: procurando eventos pendentes...&lt;br&gt;
[mais algumas centenas de milhares de linhas no mesmo segundo]&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
&lt;code&gt;time.Duration&lt;/code&gt;, em Go, é um &lt;code&gt;int64&lt;/code&gt; que conta &lt;strong&gt;nanossegundos&lt;/strong&gt;. O literal &lt;code&gt;5&lt;/code&gt;, num lugar que espera uma &lt;code&gt;time.Duration&lt;/code&gt;, vira &lt;code&gt;time.Duration(5)&lt;/code&gt;, que é cinco nanossegundos. O certo é &lt;code&gt;5 * time.Second&lt;/code&gt;, porque &lt;code&gt;time.Second&lt;/code&gt; é a constante que vale um bilhão.&lt;/p&gt;

&lt;p&gt;Esse é o mais inofensivo dos três, e o único que se anuncia na cara. Mas ele tem um parente perigoso: se você lê a variável de ambiente com &lt;code&gt;time.ParseDuration&lt;/code&gt;, o valor precisa vir com unidade (&lt;code&gt;"5s"&lt;/code&gt;). Se alguém setar &lt;code&gt;WORKER_INTERVAL=5&lt;/code&gt;, o parse falha, o código cai no valor padrão em silêncio, e você fica sem entender por que a configuração não pega.&lt;/p&gt;

&lt;p&gt;Ou seja, mesmo o bug bobinho tem uma versão dele que se disfarça de sucesso.&lt;/p&gt;

&lt;h2&gt;
  
  
  O padrão
&lt;/h2&gt;

&lt;p&gt;Escrevi os três esperando que o texto virasse uma lista de descuidos. Virou outra coisa.&lt;/p&gt;

&lt;p&gt;O erro engolido transformou uma falha de rede em uma entrega bem-sucedida. O log mentiroso transformou um &lt;code&gt;UPDATE&lt;/code&gt; que não aconteceu em uma confirmação. O parse silencioso transforma uma configuração inválida em um valor padrão. Nenhum dos três derruba o sistema. Todos os três fazem o sistema &lt;strong&gt;afirmar com confiança algo que não é verdade&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Software que quebra é chato. Software que mente é caro. E existe uma classe inteira de bugs, os mais difíceis de achar, cuja assinatura é exatamente essa: o caminho de falha existe, mas ele foi escrito para parecer o caminho de sucesso.&lt;/p&gt;

&lt;p&gt;O antídoto não é elegante. É desconfiar de todo lugar onde você decide, você mesmo, o que significa "deu certo": todo &lt;code&gt;err&lt;/code&gt; que você não propaga, todo log de sucesso que não está guardado atrás de uma verificação, todo valor padrão que substitui uma entrada inválida sem avisar.&lt;/p&gt;

&lt;p&gt;Se o seu sistema promete confiabilidade, ele precisa começar sendo honesto consigo mesmo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;O código é aberto: &lt;a href="https://github.com/Rupelio/hooksafe" rel="noopener noreferrer"&gt;github.com/Rupelio/hooksafe&lt;/a&gt;. É um projeto de estudo, e as decisões de arquitetura estão registradas em ADRs no repositório, incluindo a chave de idempotência, que errei duas vezes antes de acertar. Mas essa é outra história.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>programming</category>
      <category>go</category>
      <category>webdev</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
