Checkpoint não é idempotência: Serverless Under Constraints, Estudo 1

Raphael Moura | 19 de out. de 2026 min de leitura

Um reembolso de R$ 100,00 aprovado. O caller chama o provider, o provider grava a transação no ledger 145 milissegundos depois, e a resposta não chega. Com 3 segundos sem resposta, o caller desiste, registra o timeout e entrega o problema ao mecanismo de retry. Um minuto depois, o retry chama o provider de novo, o provider grava de novo, e o cliente recebe R$ 200,00. A execução termina em ExecutionSucceeded, a mensagem sai da fila sem passar pela DLQ, e nenhum alarme tem motivo para disparar.

Rodei esse cenário numa conta AWS de sandbox em us-east-1, no dia 7 de outubro, com a evidência congelada e publicada. É o primeiro estudo da série Serverless Under Constraints, e ele nasceu de uma frase minha: no artigo sobre Lambda Durable Functions, escrevi que tudo que não deve repetir precisa estar dentro de um step. Estar no step é necessário, e o texto deixava entender que bastava. O estudo mostra quanto custa essa diferença.

O resultado ambíguo

O Estudo 1 trata do resultado ambíguo: o caller despachou a chamada e ficou sem resposta autoritativa. Do ponto de vista dele, “o provider não recebeu” e “o provider executou e a resposta se perdeu” são indistinguíveis, e o timeout só informa que ele parou de esperar. O Brandur Leach descreveu exatamente essa situação em 2017, no blog de engenharia da Stripe, e é por causa dela que a API da Stripe aceita chave de idempotência.

Quem já fez um Pix com o app travado em “processando” conhece a sensação. A pessoa sensata abre o extrato antes de mandar de novo. O retry automático recebe um erro e repete a operação, porque foi configurado para isso, sem extrato nenhum para conferir.

E em serverless o retry mora em toda camada: no SDK, que repete por padrão, inclusive em timeout do lado do cliente, no event source mapping, que devolve o lote à fila quando a função falha, no Retry do Step Functions e na retry strategy de cada step do Durable. Cada camada que repete uma chamada com efeito externo pode duplicar esse efeito. Numa leitura, isso custa milissegundos; num reembolso, custa dinheiro que alguém vai ter que ir buscar de volta.

A pergunta do estudo: quando o provider grava o reembolso antes de o caller estourar o deadline, cada estratégia de execução preserva a regra “um reembolso aprovado gera exatamente um efeito monetário, dentro do valor capturado”? Com um provider sem chave de idempotência, o reembolso em dobro era previsível, e a hipótese, escrita na especificação antes do run, dizia isso. O que o estudo testa é se algum mecanismo do lado do caller (checkpoint, fila FIFO, retry com delay) muda esse resultado, e se o sistema percebe o estrago.

Serverless Under Constraints

Para responder perguntas desse tipo com evidência, montei um laboratório aberto. A série testa estratégias de arquitetura serverless sob falha, pico de tráfego, limite de custo, concorrência e requisito de consistência. Cada estudo coloca variantes de arquitetura diante do mesmo cenário de negócio e do mesmo tratamento controlado, em recursos de verdade numa conta AWS de sandbox; emulador ajuda a desenvolver e fica fora da evidência final. Um oráculo independente lê a evidência congelada e devolve pass, fail ou indeterminate para uma invariante de negócio, como dinheiro, pedido ou estoque, e uptime e taxa de erro entram só como diagnóstico. Protocolo, código, evidência redigida e limitações ficam públicos, para que qualquer pessoa repita o experimento e conteste a conclusão.

O desenho do Estudo 1

As duas variantes consomem uma fila SQS FIFO própria, com batch size um, maxReceiveCount igual a dois e DLQ FIFO, e chamam o mesmo provider de forma síncrona, via Lambda Invoke, por um provider client compartilhado: deadline de aplicação de 3 segundos e retries do SDK desligados (maxAttempts: 1), para que o único retry em jogo seja o da arquitetura. Muda quem é dono do retry:

  • Convencional (Lambda + SQS): no timeout, a função relança o erro, a invocação falha, e o SQS entrega a mensagem de novo quando o visibility timeout de 60 segundos expira.
  • Durable (Lambda Durable Functions): a função tem um único step, refund-attempt, com retry strategy explícita de duas tentativas, 60 segundos de delay e nenhum jitter. A segunda tentativa acontece dentro da mesma execução durável. A fila dessa variante usa visibility timeout de 360 segundos, acima do execution timeout de 300, porque o event source mapping só dá a mensagem como processada quando a execução inteira termina.

O handler do Durable, simplificado a partir do código do estudo:

import { StepSemantics, withDurableExecution } from '@aws/durable-execution-sdk-js';

export const handler = withDurableExecution(async (event, context) => {
  const delivery = parseDelivery(event);
  return context.step(
    'refund-attempt',
    (step) => caller.runStepAttempt(delivery, step.attempt),
    {
      retryStrategy: (_error, attemptsMade) =>
        attemptsMade < 2
          ? { shouldRetry: true, delay: { seconds: 60 } }
          : { shouldRetry: false },
      semantics: StepSemantics.AtLeastOncePerRetry,
    },
  );
});

Repara na assinatura da retry strategy: um Error e o número de tentativas já feitas. É toda a informação que o mecanismo tem para decidir se repete.

No cenário CONTROL, o provider grava e responde na hora. No COMMIT_THEN_TIMEOUT, o provider grava a transação de forma atômica no ledger e segura a resposta atrás de uma barreira. O caller estoura os 3 segundos e registra o timeout no próprio journal; um controller independente lê esse registro pelo DynamoDB Stream, valida, e só então sinaliza ao provider que pode liberar a resposta. A liberação depende do timeout registrado, numa cadeia causal que o oráculo verifica, e a ordem “commit antes do timeout” é imposta pelo desenho e conferida por relógio de parede. Antes de qualquer trial, um probe de transporte, uma execução isolada com uma única chamada, precisou provar em cloud real que esse mecanismo funciona.

O provider foi desenhado sem chave de idempotência, sem deduplicação e sem limite de chamadas. É a escolha que define o estudo: ela expõe de propósito o retry da arquitetura.

O veredito vem de um oráculo independente, que lê um snapshot fortemente consistente do ledger do provider e aplica três regras monetárias: exatamente uma transação de sucesso por pedido de reembolso, soma reembolsada dentro do valor capturado, e conjunto de transações igual ao efeito autorizado, com valor, moeda e identidades corretos.

Toda regra que muda veredito precisava estar implementada no oráculo antes da primeira evidência de cloud, porque oráculo ajustado depois de ver os dados escreve a conclusão em vez de medir. O protocolo também proíbe alterar ou excluir trial que contrarie a hipótese. A evidência fica congelada em pacotes identificados pelo SHA-256 do índice, e cada valor publicado cita o artefato e o digest de onde foi lido.

Duas variantes, o mesmo reembolso em dobro

O run canônico executou quatro trials em sequência. Os quatro foram válidos, o cleanup terminou, o leak audit (a varredura que procura recurso do estudo esquecido na conta) saiu limpo, e a comparação entre variantes foi elegível, com entradas e tratamento iguais nas duas e evidência íntegra:

TrialCenárioVarianteTransações no ledgerReembolsadoVeredito
1CONTROLconvencional1R$ 100,00pass
2CONTROLdurable1R$ 100,00pass
3COMMIT_THEN_TIMEOUTconvencional2R$ 200,00fail
4COMMIT_THEN_TIMEOUTdurable2R$ 200,00fail

Nos controles, a chamada ao provider terminou em 158 e 238 ms, com pelo menos 2,7 segundos de folga até o deadline. Nos tratamentos, as duas variantes falharam nas três regras monetárias, e as validações de cada variante (execuções de dois trials, feitas antes do run para provar a implementação) reproduziram o mesmo par de resultados.

Quanto o ledger reembolsou, segundo a segundo

Total acumulado de transações de sucesso no ledger do provider, em cada trial do run canônico; tratamento é o cenário COMMIT_THEN_TIMEOUT. A área tingida fica acima do valor aprovado, e a linha vertical marca o timeout do caller: nos tratamentos, o primeiro commit acontece antes do timeout, e o retry leva o total a R$ 200,00.
Ver os dados em tabela
TrialVarianteCenárioTentativaCommit confirmadoO caller registrouTotal no ledger
1ConvencionalCONTROL10,212 sSUCCEEDEDR$ 100,00
2DurableCONTROL10,154 sSUCCEEDEDR$ 100,00
3ConvencionalCOMMIT_THEN_TIMEOUT10,212 sTIMED_OUT (3,029 s)R$ 100,00
3ConvencionalCOMMIT_THEN_TIMEOUT259,275 sSUCCEEDEDR$ 200,00
4DurableCOMMIT_THEN_TIMEOUT10,145 sTIMED_OUT (3,029 s)R$ 100,00
4DurableCOMMIT_THEN_TIMEOUT264,241 sSUCCEEDEDR$ 200,00

O trial 4, o tratamento do Durable, conta a história inteira em oito linhas. Juntei eventos de três fontes independentes: o journal do caller, o journal do provider e o histórico da execução durável que a própria AWS expõe (horários em UTC):

06:16:50.248  caller    dispatch_started            tentativa 1
06:16:50.393  provider  provider_commit_confirmed   R$ 100,00 no ledger
06:16:53.277  caller    caller_timeout_recorded     ~3 s sem resposta
06:16:53.384  durable   StepFailed                  attempt 1, próxima em 60 s
06:17:53.509  durable   StepStarted                 refund-attempt
06:17:54.489  provider  provider_commit_confirmed   R$ 100,00 no ledger
06:17:54.563  durable   StepSucceeded               attempt 2
06:17:54.593  durable   ExecutionSucceeded

Nenhum componente violou o próprio contrato, e o cliente recebeu o dobro.

A variante convencional chegou ao mesmo lugar por outro caminho. O provider confirmou o commit 212 ms depois do dispatch, o caller estourou o deadline, a invocação falhou, e o SQS entregou a mesma mensagem de novo cerca de 56 segundos depois do timeout (o visibility timeout conta a partir do recebimento). A segunda entrega chamou o provider, que gravou de novo. Por incrível que pareça, a fila era FIFO, e o “exactly-once processing” do FIFO não ajudou em nada. A deduplicação dele impede que um envio repetido coloque uma segunda cópia da mensagem na fila; aqui existia uma única mensagem, entregue duas vezes porque ninguém a apagou depois da primeira.

Sucesso no status, o dobro no extrato

Nos dois trials de tratamento, o estado final que o sistema reporta é sucesso: pedido em FINISHED com motivo SUCCEEDED, DLQ vazia em todas as observações, execução durável em ExecutionSucceeded. Na convencional, a única marca operacional é uma invocação com erro que o retry “curou” menos de um minuto depois, e erro transitório que se resolve sozinho é exatamente o que a maioria dos alarmes trata como ruído. Só o journal instrumentado do estudo guarda que o conhecimento sobre o efeito terminou UNKNOWN.

Cada tentativa contra o deadline de 3 segundos

Tempo entre o dispatch e o fim de cada tentativa, no run canônico e nas validações. Os controles terminam com folga, a primeira tentativa sob tratamento para no deadline, e o retry volta com sucesso em menos de um segundo.
Ver os dados em tabela
GrupoVariantePacoteResultadoDispatch até o fimFolga até o deadline
ControleConvencionalrun canônico abf41ffdSUCCEEDED237,7 ms2.762,3 ms
ControleConvencionalvalidação a8a5b2f1SUCCEEDED250,3 ms2.749,7 ms
ControleDurablerun canônico abf41ffdSUCCEEDED158,5 ms2.841,5 ms
ControleDurablevalidação 60511328SUCCEEDED233,6 ms2.766,4 ms
Tratamento, 1ª tentativaConvencionalrun canônico abf41ffdTIMED_OUT3.002,6 ms-2,6 ms
Tratamento, 1ª tentativaConvencionalvalidação a8a5b2f1TIMED_OUT3.002,6 ms-2,6 ms
Tratamento, 1ª tentativaDurablerun canônico abf41ffdTIMED_OUT3.003,8 ms-3,8 ms
Tratamento, 1ª tentativaDurablevalidação 60511328TIMED_OUT3.003,0 ms-3,0 ms
Tratamento, retryConvencionalrun canônico abf41ffdSUCCEEDED130,1 ms2.869,9 ms
Tratamento, retryConvencionalvalidação a8a5b2f1SUCCEEDED164,4 ms2.835,6 ms
Tratamento, retryDurablerun canônico abf41ffdSUCCEEDED868,7 ms2.131,3 ms
Tratamento, retryDurablevalidação 60511328SUCCEEDED151,0 ms2.849,0 ms

O gráfico mostra o que o caller viveu: uma tentativa que bateu no deadline e um retry que voltou com sucesso em menos de um segundo. Para ele, o retry funcionou. O oráculo diz fail porque olha para o ledger, que tem duas transações de R$ 100,00 para um reembolso aprovado de R$ 100,00, contra um pagamento capturado de R$ 100,00. O estudo deriva uma métrica para essa contradição, correct_completion, verdadeira só quando o veredito é pass e o processamento terminou com sucesso. Nos dois tratamentos, ela é false com o processamento reportando SUCCEEDED.

Particularmente falando, essa é a lição que vale até para quem nunca vai usar Durable Functions. A telemetria do sistema sob teste registra o que o sistema acredita ter feito, e por isso não serve de oráculo para efeito externo. Se você move dinheiro, a verificação que importa em produção é a reconciliação contra o ledger de quem efetivamente moveu o dinheiro. É o Pix travado em “processando” de novo: quem responde quantas vezes o dinheiro saiu é o extrato.

Por que o checkpoint não salvou

Checkpoint guarda o que a função sabe, e o resultado ambíguo é justamente o que ela não sabe. Quando um step termina, o resultado vai para o log de checkpoint e o step nunca mais executa. Quando a chamada dentro do step estoura o deadline, não existe resultado para gravar, e a retry strategy decide só com o erro que recebeu, como mostra a assinatura lá em cima.

A flag de semântica do step não muda isso. AtLeastOncePerRetry, o padrão, e AtMostOncePerRetry tratam do que acontece quando a função é interrompida no meio de uma tentativa e o replay chega àquele step; a decisão de tentar de novo depois de um erro continua com a retry strategy. A própria AWS é explícita: a página de idempotência do Lambda avisa que steps têm semântica at-least-once por padrão, e a de boas práticas completa que as duas semânticas valem por tentativa, que nenhuma garante execução única no workflow inteiro e que uma retry strategy que repete em caso de falha executa o step de novo mesmo com at-most-once. O problema está no modelo mental, que meu artigo anterior ajudou a espalhar, de que colocar a chamada dentro do step é a proteção. O step garante que o que terminou não repete; o que não terminou fica por sua conta.

Lendo o SDK com esse resultado na mão, chama atenção o que acontece quando ninguém configura nada: sem retry strategy explícita, o SDK de TypeScript cai no preset padrão, com até seis tentativas, backoff exponencial e jitter. Um step que chama um provider de pagamento sem retry strategy declarada pode chamar esse provider seis vezes. As camadas também se multiplicam: a mesma página de idempotência informa que event source mapping não oferece idempotência para execuções duráveis, então cada nova entrega do SQS começa uma execução nova, sem nenhum checkpoint da anterior. No caminho configurado do estudo, duas tentativas por execução vezes duas entregas da fila dão até quatro chamadas ao provider no Durable, contra duas na convencional, e o at-least-once ainda pode somar mais. No trial 4, a segunda tentativa teve sucesso e a fila nem precisou entregar de novo.

O Durable tem ganhos e custos reais. A segunda tentativa aconteceu dentro da mesma execução, sem nova entrega da fila, e o histórico mostra StepFailed seguido de StepSucceeded com o delay exato configurado. O retry virou código, com nome, delay e número de tentativas, e aparece no code review e no histórico da execução. Em troca, a mensagem fica em voo durante a execução inteira, o visibility timeout precisa ser dimensionado pela execução, e, alimentada por event source mapping, a execução inteira fica limitada ao timeout máximo da função: 15 minutos no modo on-demand, 90 em Lambda Managed Instances. A espera longa, que é o grande apelo do Durable, encolhe quando a entrada é uma fila.

O estudo não elege vencedor, e eu não vou fingir que elege. Num eixo só, a legibilidade do retry, prefiro o que aparece no histórico da execução ao que acontece porque um visibility timeout expirou.

Onde eu colocaria a defesa

O estudo excluiu de propósito idempotência no provider e reconciliação. O que segue é posição minha, sustentada pelo mecanismo que o estudo expôs e pelo que a indústria de pagamentos já pratica, ainda sem medição da série.

A defesa precisa estar onde o efeito acontece. Uma regra não monetária passou nos dois tratamentos: as duas tentativas carregaram o mesmo refund_request_id, ref-poc-001. A identidade lógica estava lá; faltou alguém do outro lado da chamada usá-la. Na API da Stripe, um retry com a mesma Idempotency-Key recebe a resposta salva da primeira execução, inclusive quando ela foi um erro 500. A API Pix do Banco Central modela a devolução como PUT /pix/{e2eid}/devolucao/{id}, com o id gerado pelo cliente: um id já usado para aquele Pix é rejeitado, e um GET no mesmo caminho consulta o resultado. Chave e reconciliação no mesmo contrato. É também o desenho que a AWS descreve no Builders’ Library, com um identificador de requisição fornecido pelo caller, e que o EC2 implementa com o ClientToken. Recomendo muito levar isso para o design review como critério de seleção de provider: se a API que move dinheiro não aceita chave de idempotência, nenhuma configuração de retry do seu lado elimina o risco.

Idempotência no seu handler não resolve este caso. O utilitário de idempotência do Powertools for AWS Lambda protege contra a mesma entrada processada duas vezes, e a documentação dele avisa que exceção não tratada apaga o registro de idempotência, para que a próxima invocação com o mesmo payload execute de novo. É o caminho exato do trial 3: o timeout de aplicação virou erro, a invocação falhou, e a segunda entrega teria sido liberada para chamar o provider. Deduplicar a entrada do seu lado deixa o efeito do lado de lá duplicado do mesmo jeito.

E o desconhecido precisa virar estado de domínio. No estudo, o conhecimento sobre o efeito vira UNKNOWN no primeiro timeout e nunca mais sai de lá; um sucesso posterior prova apenas o efeito posterior. Num fluxo de dinheiro, timeout depois do dispatch deveria abrir reconciliação, consultando o provider pela sua referência antes de qualquer nova tentativa. Na retry strategy do exemplo, isso vira uma condição sobre o erro: timeout depois do dispatch devolve shouldRetry: false, e o fluxo segue para a reconciliação. A própria Stripe, que tem chave de idempotência, orienta parar de tentar e reconciliar quando a janela de retry acaba sem resultado. Desligar o retry sem reconciliar troca o risco do reembolso em dobro pelo risco do reembolso que nunca acontece. Consultar antes de repetir também tem janela de corrida, porque a primeira chamada pode ainda estar em voo no provider, e por isso reconciliação complementa a chave de idempotência; sozinha, ela deixa essa janela aberta.

Limites e como contestar

Um trial por célula, ordem fixa e um provider controlado servem para validar protocolo e mecanismo. Com que frequência essa janela abre em produção, eu não tenho dado para afirmar. O provider reproduz a fronteira causal e monetária que interessa, e um processador de pagamento real tem muito mais comportamento. A ordem “commit antes do timeout” é conferida por relógio de parede entre funções Lambda diferentes, sob uma premissa de alinhamento de relógio declarada no protocolo, sem garantia formal da AWS.

A proveniência também tem limite, e ele está escrito no close-out: nenhum timestamp de terceiro antecede a evidência. O repositório não tinha CI, e os commits do oráculo só chegaram ao GitHub depois de a evidência estar congelada. A ordem “oráculo antes da evidência” se apoia em datas de commit, em registros congelados dentro dos pacotes e em replays do oráculo antigo sobre esses pacotes, e quem atesta isso sou eu. O custo faturado ficou unverified: a estimativa antes do billing foi de US$ 0,28 para o run, contra um teto declarado de US$ 5,00, e a conta de sandbox não tinha export de billing para confirmar.

O caminho deixou ainda duas pegadinhas da AWS registradas. Quando você deleta uma tabela DynamoDB com stream habilitado, o stream vai para o estado DISABLED e só é apagado 24 horas depois, sem chamada que o apague antes; enquanto isso, continua legível e com tags. O leak audit do probe de ensaio não conseguiu provar a quem pertencia esse stream, fechou como inconclusive, e o fechamento precisou de uma emenda de recuperação. Além disso, ListDurableExecutionsByFunction recusou alias como qualifier, com a mensagem “Cannot filter by alias”; a referência atual da API descreve o parâmetro como versão, e a da AWS CLI v1 ainda fala em “version or alias”. A primeira validação do Durable tentou listar as execuções pelo alias, não conseguiu provar que elas tinham terminado, e os trials congelaram como indeterminate. A validação foi excluída pelo critério de reprodução e continua listada no close-out, com o motivo. Execução que dá errado também é dado.

Tudo está no repositório do Serverless Under Constraints: especificação, código das variantes e do oráculo, um results.json em que cada valor cita o artefato e o SHA-256 de onde foi lido, e uma cópia redigida dos pacotes de evidência, em que só o account id e caminhos locais foram substituídos. A primeira verificação precisa apenas de tar, jq e shasum, sem conta AWS:

cd public/study-1-evidence
tar -xzf study-1-evidence.tar.gz
jq -r '.files[] | "\(.redacted_sha256)  \(.path)"' redaction-manifest.json \
  | shasum -a 256 -c --quiet -

O comando confere cada arquivo da cópia contra o manifesto de redação. O README traz o restante (identidade de cada pacote contra o results.json, arquivos alterados pela redação, registros de verificação), e o close-out do estudo separa o que um leitor prova a partir dos bytes públicos do que fica apenas atestado.

O que aprendemos

Juntando resultado, mecanismo e limites, sai daqui uma mudança no jeito de projetar e outra no jeito de conduzir a série. Para quem projeta, as regras cabem numa lista curta:

  1. Timeout depois do dispatch é UNKNOWN. É estado de domínio, com saída para a reconciliação e nunca para retry cego.
  2. Retry automático de efeito irreversível só com chave de idempotência no provider, derivada da identidade de negócio e igual em todas as camadas.
  3. Retry strategy sempre explícita, com as camadas contadas e multiplicadas: SDK, step e fila somam tentativas, e nenhum default entra sem revisão.
  4. O monitor que importa compara pedido com ledger e alarma na divergência. O par que o estudo usa, veredito de preservação e conclusão correta, funciona como métrica de produção; status de sucesso fica como diagnóstico.

Para o laboratório, os limites listados acima viram lições de método, e algumas doeram. A ordem “oráculo antes da evidência” ficou atestada em vez de provada, e a correção é simples: publicar o commit do oráculo com tag antes da primeira execução em cloud, para que o GitHub sirva de testemunha. Seis itens da especificação foram ratificados umas quinze horas depois do run, descrevendo o que o código já fazia, e a ratificação precisa vir antes. Tag de alocação de custo e export de billing precisam estar ativos antes do deploy, para que a fatura do run seja atribuível quando chegar, com até um dia de atraso. A qualificação em etapas, com probe de transporte, validação por variante e só então o run canônico, se pagou: o probe de ensaio pegou o stream residual do DynamoDB e a primeira validação pegou o problema do alias, os dois antes do run que conta.

As perguntas que ficam abertas são as candidatas naturais para os próximos estudos. O que muda quando o provider aceita chave de idempotência, tanto no estilo da Stripe, que devolve a resposta salva, quanto no estilo do Pix, que rejeita o id repetido e obriga o caller a consultar o resultado? Reconciliar antes de repetir, sem chave, fecha a janela de corrida ou só a estreita? O Durable que esgota as tentativas e volta pela fila chega mesmo às quatro chamadas do caminho configurado? E com latência realista, sem tratamento forçado, com que frequência o commit chega antes do timeout? Essa última exige repetição e desenho estatístico, que um trial por célula não sustenta.

Fechando

Sob COMMIT_THEN_TIMEOUT, Lambda com SQS e Lambda Durable Functions devolveram R$ 200,00 para um reembolso aprovado de R$ 100,00, e as duas terminaram reportando sucesso. O retry decidiu quando o segundo efeito aconteceria, sem ter como saber se ele deveria acontecer. As garantias da plataforma (at-least-once, checkpoint, FIFO) operam sobre mensagens e execuções, a invariante de negócio opera sobre efeitos, e é nesse espaço entre as duas que a série vai continuar procurando.

Pega um fluxo do seu sistema que mova dinheiro, estoque ou qualquer efeito externo irreversível e responde por escrito: se a chamada ao provider estourar o timeout depois do commit, o que impede o retry de duplicar? Procura a resposta no contrato da API do provider antes de procurar no seu handler. Se o contrato não aceita chave de idempotência nem consulta por referência, você acabou de achar o risco que nenhum dashboard vai te mostrar. E se quiser contestar o resultado, clona o repositório e roda a verificação a partir dos bytes.

Referências