Pular para o conteúdo principal

Limites de taxa

Existem dois limites independentes, e eles são aplicados nesta ordem:

  1. Por IP do cliente — 100 requisições / 60 s, verificado antes de a sua chave de API sequer ser lida.
  2. Por tenant, por endpoint — verificado depois da autenticação.

Estourar qualquer um dos dois retorna 429 com o código de erro rate_limited. A maioria dos integradores só lê a segunda tabela e depois não consegue explicar os próprios 429, então comece pela primeira.

Camada 1 — por IP

EscopoLimiteJanela
Um IP de cliente, somando todas as rotas com limite de taxa100 requisições60 s

Isso se aplica a todo endpoint que aceita tráfego do dispositivo do usuário:

  • POST /v1/verify/init
  • PUT /v1/verify/upload/:verificationId/:role
  • GET /v1/verify/challenge/:verificationId/next
  • POST /v1/verify/submit
  • GET /v1/verify/:id

Não se aplica a GET /health nem a requisições de preflight OPTIONS.

O IP é obtido do cabeçalho cf-connecting-ip da Cloudflare — o endereço real do cliente, não o de qualquer proxy que você coloque na frente da sua própria aplicação.

Por que essa camada roda antes da autenticação

Autenticar custa uma leitura de KV. Se uma enxurrada não autenticada precisasse ser autenticada antes de poder ser rejeitada, a enxurrada ainda nos custaria a consulta. Então o contador por IP roda primeiro.

Vale dizer com todas as letras a consequência disso para você: um 429 pode chegar em uma requisição cuja chave de API nunca foi verificada. Isso não é evidência de que a sua chave é válida, e não é atribuível a nenhum tenant. Se você está depurando um 429 e os seus números por tenant estão longe dos limites abaixo, é esta a camada em que você está batendo.

Camada 2 — por tenant, por endpoint

EndpointLimiteJanela
POST /v1/verify/init60 requisições60 s
POST /v1/verify/submit30 requisições60 s
GET /v1/verify/:id600 requisições60 s

Cada endpoint tem o seu próprio contador. Gastar o seu orçamento de init não toca no seu orçamento de status.

PUT /v1/verify/upload/... e GET /v1/verify/challenge/.../next não têm limite por tenant nenhum — eles se autenticam com um token de upload por verificação, não com uma chave de API, então não há tenant contra o qual contabilizar. São governados apenas pelo limite por IP. É exatamente por isso que o limite por IP importa mais do que parece.

Esses limites não são ajustáveis por tenant hoje

Os números acima estão fixos nas definições de rota do Worker. Não há campo de override por tenant, então "vamos aumentar o seu limite" não é algo que o suporte consiga fazer sem publicar um deploy. Se o seu volume genuinamente precisa de mais, diga cedo — mas planeje contra estes números, não contra uma exceção prometida.

A janela é fixa, não deslizante

O contador agrupa por minuto de relógio (floor(unix_seconds / 60)), não por uma janela móvel de 60 segundos a partir da sua primeira requisição. Duas consequências práticas:

  • O reset é na virada do minuto. Se você bater no limite às 12:04:59, você está liberado um segundo depois, não 60 segundos depois.
  • Uma rajada pode atravessar a fronteira. 100 requisições às 12:04:59 mais 100 às 12:05:00 são todas permitidas — 200 requisições em dois segundos, todas legais. Não construa um teste de carga que conclua que o limite é 200; não construa um cliente que dependa de poder fazer isso.

Como é um 429

{
"error": "rate_limited",
"message": "Rate limit exceeded — retry later",
"requestId": "9f511d92ac11236d-SJC",
"detail": {
"retry_after": 60
}
}

O mesmo valor está no cabeçalho de resposta Retry-After, em segundos. Leia o cabeçalho em vez de cravar um atraso no código:

if (response.status === 429) {
const waitSec = Number(response.headers.get('Retry-After') ?? 60);
await new Promise(r => setTimeout(r, waitSec * 1000));
// então tente de novo — veja abaixo a nota sobre qual camada você atingiu
}

rate_limited é um dos poucos erros da Veridia em que uma nova tentativa genuinamente vale a pena. Os outros são backend_unavailable (503) e internal_error (500). Todo o resto é erro de cliente, e tentar de novo apenas reproduz o mesmo erro. Veja Erros.

O problema do CGNAT — leia antes de lançar para mobile

Este é o modo de falha que mais vemos, e ele não é óbvio a partir das tabelas acima.

Uma verificação não é uma requisição. Conte quanto um único usuário de fato gasta do orçamento por IP:

FluxoRequisições a partir do dispositivo do usuário
Padrão (frente do documento + selfie)~4: init, 2 uploads, submit
Padrão com verso do documento~5
Com activeLiveness: true~26: init, 3 uploads de documento/selfie, 1 upload de âncora de prova de vida, 16 uploads de frames de prova de vida (4 etapas x 4 frames), ~4 chamadas de desafio /next, submit

O número da prova de vida ativa é o que morde. O desafio são 4 etapas de pose com uma rajada de 4 frames cada, e cada frame é um PUT próprio. Isso é por design — os frames são a base sobre a qual a checagem anti-injeção é construída — mas significa que uma verificação com prova de vida custa aproximadamente um quarto do orçamento por IP.

Agora coloque vários usuários atrás de um único IP de saída. Esse é o caso normal na América Latina: o carrier-grade NAT (CGNAT) coloca milhares de assinantes móveis atrás de um punhado de endereços públicos compartilhados. Escritórios corporativos, redes universitárias e Wi-Fi público fazem a mesma coisa.

A aritmética:

  • Fluxo padrão: cerca de 20 usuários simultâneos por minuto por IP compartilhado antes de alguém ver um 429.
  • Prova de vida ativa: cerca de 3 usuários simultâneos por minuto por IP compartilhado.

Quando acontece, falha no meio da captura — o usuário já fotografou o documento e está sendo informado de que algo deu errado. E, porque é a camada por IP, isso vai acontecer com usuários que compartilham um endereço com a sessão de outra pessoa, o que faz parecer aleatório.

Preferimos que você soubesse disso a que descobrisse. É um teto real no desenho atual, não um botão de ajuste que esquecemos de girar.

O que fazer a respeito

  • Não repita uploads de forma agressiva. Um cliente que tenta um PUT que falhou três vezes transforma o 429 de um usuário em quatro, e empurra o IP compartilhado ainda mais para além do limite. Tente uma vez, com o atraso do Retry-After, e então mostre o erro.
  • Trate rate_limited no evento de erro do widget e mostre uma mensagem do tipo "a rede está congestionada, tente de novo em instantes" em vez de uma falha genérica. A próxima tentativa do usuário muito provavelmente vai dar certo, porque a janela reseta na virada do minuto.
  • Habilite activeLiveness onde ele compensa o seu custo, não em todo lugar. É o sinal anti-injeção mais forte disponível, e também é 6x o volume de requisições. Onboarding de alto valor: sim. Reverificação de baixo risco: provavelmente não.
  • Faça polling do seu servidor, não do navegador. GET /v1/verify/:id exige uma chave secreta de qualquer forma, então já é server-side — o que significa que o orçamento de 600/minuto é gasto a partir do IP do seu servidor, não do dos seus usuários. Mantenha assim.
  • Conte para nós o formato do seu tráfego antes do lançamento se você espera volume mobile concentrado. Não conseguimos elevar o limite por tenant hoje, mas preferimos planejar o deploy junto com você a ler sobre isso em um incidente.

Qual camada eu atingi?

O corpo do 429 é idêntico para as duas, então use isto:

SintomaCamada
Você está bem abaixo de 60 init/min no tenant inteiro, mas usuários individuais falhamPor IP. Vários usuários compartilham um endereço de saída.
429 em PUT .../upload/... ou no endpoint de desafioPor IP, sempre — essas rotas não têm limite por tenant.
O 429 chega mesmo com uma chave de API revogada ou malformadaPor IP — a chave nunca foi verificada.
O seu próprio servidor, um IP, chamando GET /v1/verify/:id em um loop apertado de pollingPode ser qualquer uma das duas. 100/min por IP morde muito antes de 600/min por tenant.
init em massa a partir do seu backend, acima de 60/minPor tenant.

Repare na quarta linha: se você faz polling de um único servidor, o limite por IP de 100 é o teto efetivo do polling, não os 600 da tabela por tenant. Um intervalo de polling de 500 ms são 120 requisições/minuto e será limitado. Faça polling a 1 s ou mais lento, ou use webhooks e pare de fazer polling.

Ficando abaixo dos limites

  • Prefira webhooks a polling. Um webhook são zero requisições. Um polling de 30 segundos a 1 Hz são 30.
  • Aumente progressivamente o intervalo de polling. Comece em ~1 s e cresça; um veredicto tipicamente chega em 2-3 segundos, então um loop fixo e apertado gasta orçamento principalmente na cauda.
  • Nunca repita um 4xx que não seja 429. invalid_body, verification_not_found, secret_key_required e insufficient_credits vão devolver a mesma resposta toda vez, e cada nova tentativa ainda conta contra os dois limites.
  • Não chame /init especulativamente. Chame quando o usuário de fato iniciar o fluxo. Um init por visualização de página é um jeito fácil de gastar 60/minuto com pessoas que nunca abrem a câmera.

O que vem a seguir

  • Erros — o catálogo completo de erros, incluindo quais códigos vale tentar de novo
  • Autenticação — tipos de chave, e por que o endpoint de resultados é somente server-side
  • Webhooks — a forma de parar de fazer polling por completo