8 min de leituraEngenharia

API de Encurtador: Limites de Taxa, Retries e Idempotência

Como chamar uma API de encurtador em produção: limites por token bucket, quais status retentar com backoff e chaves de idempotência contra duplicatas.

Marius Voß
DevRel · edge infra

Três endpoints, um header de autenticação, um corpo JSON. Uma API de encurtador de URL é uma das integrações mais simples em qualquer backlog, e o quickstart lhe permite ter um link curto funcional em poucos minutos. O que o quickstart omite é tudo o que acontece quando a integração opera em volume: o limitador de taxa rejeitando pedidos, um 503 transitório a meio de um lote, uma fila de trabalhos que entrega a mesma mensagem duas vezes. Erre nesses pontos e obterá links duplicados, trabalho perdido e uma tempestade de 429 que torna tudo ainda pior.

Este artigo é o complemento de endurecimento para produção do quickstart de API. Aborda os três mecanismos que separam uma demo de uma integração confiável: limites de taxa e como controlar o ritmo contra eles, quais os erros a retentar e como fazer backoff, e chaves de idempotência que impedem uma retentativa de criar um segundo link. Os exemplos usam a API do Elido, mas os padrões são os mesmos em qualquer API de encurtador bem construída. Se trata os links curtos como infraestrutura gerenciada por código, o argumento mais alargado está em links curtos como Terraform.

Limites de Taxa: um Token Bucket e um Header Retry-After

O Elido mede a API com um token bucket, com âmbito por workspace. As taxas sustentadas são 10 pedidos por segundo no Free, 30 no Pro e 100 no Business, com capacidades de burst de 20, 60 e 200. No Pro, por exemplo, um bucket cheio permite disparar 60 pedidos de uma vez antes de a taxa voltar aos 30 por segundo sustentados. Por cima existe um bucket separado por chave de API, para que uma chave ruidosa não consuma todo o workspace. A maioria dos trabalhos de criação de links cabe no burst e nunca sente o limite.

Não tem de adivinhar quando um limite é atingido. Um pedido rejeitado traz três sinais:

  • Retry-After - quantos segundos esperar antes de tentar de novo (atualmente 1).
  • X-RateLimit-Scope - qual bucket foi atingido: workspace ou api_key.
  • O corpo JSON - {"error":"workspace rate limit exceeded"}, que nomeia o limite.

Não existem headers com os tokens restantes, por isso controle o ritmo no cliente: mantenha a concorrência igual ou abaixo da taxa sustentada do seu plano e, ao primeiro 429, pare e aguarde Retry-After em vez de correr contra uma parede de rejeições. Um ritmo antecipado mantém o débito suave; retentar às cegas após cada rejeição desperdiça viagens e, se todos os clientes retentam no mesmo instante, fabrica uma debandada.

Um token bucket reabastecendo à taxa do workspace enquanto os pedidos consomem tokens, e uma resposta 429 com header Retry-After quando o bucket esvazia

Quando precisar de criar muitos links, não itere o endpoint de criação individual. POST /v1/workspaces/{workspace_id}/links/bulk aceita até 100 links num pedido. Uma chamada bulk move cem links numa só viagem, enquanto cem chamadas individuais gastariam cem tokens e a maior parte do burst. É assim que a importação do Google Sheets move os links de uma campanha sem acionar o limitador.

Um 429 Too Many Requests - o status que o RFC 6585 reserva precisamente para isto - volta com um header Retry-After que indica quantos segundos esperar. Respeite-o. Esse número é o limitador a dizer quando um token estará disponível, o que é melhor informação do que qualquer palpite do seu backoff.

Retentativas: Quais Códigos e Como Fazer Backoff

Nem todos os erros valem a pena retentar, e retentar o errado é como uma falha pequena se torna uma interrupção. Divida as respostas em dois grupos.

Retente estes, porque são transitórios: 429 (foi demasiado rápido), e 500, 502, 503, 504 (uma falha do lado do servidor ou do gateway que pode resolver-se por si). Não retente estes, porque o mesmo pedido vai falhar de forma idêntica: 400 (o payload é inválido), 401 (o token está ausente ou errado), 403 (o token não tem o scope), 404 (o recurso não existe ou não é seu), e 409 (conflito de slug ou edição com versão obsoleta). O primeiro grupo é "aguarde e tente novamente." O segundo é "corrija o código ou o input." Retentar um 400 em ciclo fechado transforma um bug num ataque de negação de serviço contra si próprio.

Para os códigos retentáveis, o algoritmo que importa é o backoff exponencial com jitter. O backoff exponencial simples - duplicar a espera a cada tentativa - ainda sincroniza clientes, porque todos os que falharam no mesmo momento também retentam nos mesmos momentos. Adicionar aleatoriedade dispersa-os. O artigo da AWS sobre exponential backoff and jitter é a referência canônica e mostra porque a versão com jitter reduz dramaticamente a contenção. Uma versão compacta em TypeScript:

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function withRetry<T>(
  call: () => Promise<Response>,
  max = 5,
): Promise<Response> {
  let attempt = 0;
  while (true) {
    const res = await call();
    if (res.ok || !RETRYABLE.has(res.status) || attempt >= max) return res;

    // Honor server guidance first; otherwise back off exponentially with full jitter.
    const retryAfter = Number(res.headers.get("retry-after"));
    const base =
      Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(1000 * 2 ** attempt, 20_000);
    const wait = Math.random() * base; // full jitter
    await new Promise((r) => setTimeout(r, wait));
    attempt++;
  }
}

Três aspetos tornam isto seguro em vez de perigoso. Limita as tentativas, para que uma falha persistente falhe de forma clara em vez de girar indefinidamente. Respeita Retry-After quando o servidor o envia, recorrendo ao backoff calculado apenas quando não o faz. E aplica jitter, para que um conjunto de workers recuperando do mesmo problema não se precipite em uníssono. Até agora só o SDK de Go está publicado; os SDKs de TypeScript, Python e Ruby ainda não estão nos seus registros de pacotes, por isso, nessas linguagens, este pequeno wrapper sobre HTTP simples é todo o cliente de que precisa.

Há uma regra que liga as retentativas à seção seguinte: uma retentativa de criação só é segura se a criação for idempotente. Caso contrário, cada retentativa arrisca um segundo link.

A falha clássica tem este aspeto. O seu worker cria um link curto, o link é criado, mas o 200 nunca chega de volta - a ligação cai na viagem de retorno. O worker vê um timeout, assume falha e retenta. Agora tem dois links para uma campanha. Em escala, o dashboard enche-se de /foo, /foo-1, /foo-2, e os duplicados distorcem todos os relatórios a jusante.

As chaves de idempotência fecham essa lacuna. Envie um header Idempotency-Key num pedido de mutação - qualquer string até 255 caracteres - e o servidor guarda a resposta associada a ela. Apresente a mesma chave novamente e recebe a resposta original de volta, código de status e corpo, sem a operação ser executada duas vezes. O padrão é o mesmo que a Stripe documenta para pedidos idempotentes, e é a forma padrão de tornar uma rede não confiável segura para escritas.

O detalhe que decide o sucesso ou falha é a origem da chave. Não gere uma chave aleatória por tentativa - isso anula o propósito, porque cada retentativa parece então uma operação nova. Derive-a de um identificador de negócio estável para que a mesma ação lógica produza sempre a mesma chave:

const res = await withRetry(() =>
  fetch(`https://api.elido.app/v1/workspaces/${WORKSPACE_ID}/links`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `order-${order.id}-link`,
    },
    body: JSON.stringify({ destination_url: order.landingUrl }),
  }),
);
const link = await res.json();

Agora uma retentativa do mesmo trabalho carrega order-12345-link novamente, encontra a resposta armazenada e devolve o link que já existe. Exatamente um link por encomenda, independentemente de quantas vezes a fila reentrega. É isto que permite combinar o ciclo de backoff acima com criações de forma segura: a retentativa e a chave de idempotência são duas metades da mesma garantia.

Uma fila de trabalhos at-least-once disparando dois pedidos de criação com a mesma chave de idempotência; o servidor armazena a primeira resposta e devolve-a para a segunda, criando exatamente um link

Dois limites a ter em conta. A chave tem âmbito por workspace: a mesma chave em dois workspaces cria dois links, o que é correto para uma API multi-tenant mas surpreende equipes que assumem que as chaves são globais. E a cache não é permanente - no Elido dura 24 horas com âmbito em (workspace, key). Uma retentativa dentro da janela desduplicica; uma retentativa três dias depois, de um trabalho bloqueado que finalmente processou, criará um link novo. Para lotes de vários dias, não confie apenas na chave. Persista o ID do link devolvido pelo primeiro sucesso e consulte-o antes de reeditar a criação. O IETF tem estado a normalizar este header no rascunho Idempotency-Key, e a ressalva da janela de 24 horas está também mencionada lá.

Se está implementando uma integração de API hoje e quer que sobreviva às suas próprias retentativas, comece num workspace gratuito, gere um token de conta de serviço e coloque uma chave de idempotência na sua primeira criação em vez de a acrescentar depois de os duplicados aparecerem.

Juntar Tudo

Uma chamada de criação de qualidade para produção é os três mecanismos empilhados. Ajuste o ritmo do cliente à taxa sustentada do seu plano para raramente atingir 429. Envolva a chamada em backoff jitterizado que retenta apenas os códigos transitórios e respeita Retry-After. Carregue uma chave de idempotência derivada de um ID de negócio para que a retentativa seja segura. Com fetch simples, o wrapper acima cobre os primeiros dois e fornece apenas a chave:

export async function shortenForOrder(order: Order) {
  const res = await withRetry(() =>
    fetch(`https://api.elido.app/v1/workspaces/${WORKSPACE_ID}/links`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `order-${order.id}-link`,
      },
      body: JSON.stringify({
        destination_url: order.landingUrl,
        tags: [`order:${order.id}`],
      }),
    }),
  );
  if (res.status === 429) {
    // withRetry gave up and we are still limited. Defer the job.
    throw new RetryableJobError(Number(res.headers.get("retry-after")) || 1);
  }
  if (!res.ok) throw new Error(`create failed: ${res.status}`); // non-retryable
  return res.json();
}

Nada disto é exótico. É a mesma disciplina que qualquer API com muitas escritas merece, aplicada a links. A recompensa é uma integração que se comporta corretamente sob carga em vez de corromper silenciosamente o seu inventário de links. Para o lado de leitura da mesma API - extrair dados de cliques sem sobrecarregar o limitador - os compromissos estão em webhooks versus polling para rastreamento de cliques, e a superfície completa de endpoints está na página de API e SDKs e na visão geral de soluções para programadores.

Relacionado no Blog

Perguntas frequentes

Quais são os limites de taxa numa API de encurtador de URL?

No Elido, os limites são por workspace e dependem do plano, com um token bucket: 10 pedidos por segundo no Free (burst 20), 30 no Pro (burst 60) e 100 no Business (burst 200). Por cima existe um bucket separado por chave de API. Quando um limite é atingido, a API devolve 429 com os headers Retry-After e X-RateLimit-Scope, que indica qual bucket foi atingido, para o cliente saber quanto esperar. As criações em massa aceitam até 100 links por pedido, por isso o endpoint bulk é a ferramenta certa para trabalhos grandes.

Quais os erros de API que devo retentar?

Retente apenas falhas transitórias: 429 (limite de taxa atingido), 500, 502, 503 e 504. Use backoff exponencial com jitter e um limite máximo de retentativas. Nunca retente 400, 401, 403, 404 ou 409 - são falhas de validação, problemas de autenticação ou conflitos, e o mesmo pedido vai falhar novamente. Combine as retentativas com uma chave de idempotência para que uma criação reiterada não produza um link duplicado.

O que é uma chave de idempotência e quando preciso de uma?

Uma chave de idempotência é uma string opaca que envia num pedido de mutação para que o servidor possa reconhecer uma retentativa e devolver o resultado original em vez de agir duas vezes. Precisa de uma sempre que uma criação ou atualização pode ser retentada - o que é sempre, porque as redes falham e as filas de trabalhos entregam pelo menos uma vez. Derive a chave de um identificador de negócio estável, como um ID de encomenda, para que a mesma operação lógica use sempre a mesma chave.

Como evito criar links curtos duplicados?

Envie uma chave de idempotência derivada de um identificador de negócio estável em cada criação. Se a chamada for retentada, o servidor devolve o link originalmente criado em vez de gerar um segundo. Para trabalhos que podem ser retentados dias depois - além da janela de cache de idempotência - guarde também o ID do link devolvido pelo primeiro sucesso e consulte-o antes de reeditar a criação.

Quanto tempo dura uma chave de idempotência?

No Elido, a cache de idempotência tem uma duração de 24 horas, com âmbito no workspace mais a chave. Uma retentativa dentro dessa janela devolve a resposta armazenada; uma retentativa após esse período é tratada como um pedido novo e criará um novo recurso. Para trabalhos em lote de vários dias, persista o ID do link resultante no primeiro sucesso em vez de depender apenas da chave.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Sem registro. Crie uma conta grátis e obtenha 50 links por mês.

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
url shortener api rate limits
api idempotency key
retry with exponential backoff
429 too many requests
link shortener api
idempotent requests

Continuar lendo