Elido
8 min de lecturaIngeniería

API de acortador de URL: límites de tasa, reintentos e idempotencia

Cómo llamar a una API de acortador de URL en producción: límites de tasa con token bucket, qué códigos de estado reintentar con backoff, y claves de idempotencia que evitan duplicados.

Marius Voß
DevRel · edge infra
Un token bucket midiendo las solicitudes de API, un bucle de reintento con backoff, y una clave de idempotencia deduplicando una llamada de creación duplicada, en la paleta de colores de Elido

Tres endpoints, una cabecera de autenticación, un cuerpo JSON. Una API de acortador de URL es una de las integraciones más sencillas de cualquier backlog, y el quickstart te da un enlace corto funcionando en pocos minutos. Lo que el quickstart omite es todo lo que ocurre cuando la integración funciona a volumen: el limitador de tasa empujando hacia atrás, un 503 transitorio en medio de un lote, una cola de trabajos que entrega el mismo mensaje dos veces. Gestionarlos mal produce enlaces duplicados, trabajo perdido y una tormenta de 429 que empeora las cosas.

Esta entrada es el complemento de endurecimiento en producción del quickstart de API. Cubre los tres mecanismos que separan una demo de una integración fiable: límites de tasa y cómo ajustar el ritmo contra ellos, qué errores reintentar y cómo hacer backoff, y claves de idempotencia que evitan que un reintento cree un segundo enlace. Los ejemplos usan la API de Elido, pero los patrones son los mismos con cualquier API de acortador de enlaces bien construida. Si tratas los enlaces cortos como infraestructura que gestionas desde código, el caso más amplio para eso está en enlaces cortos como Terraform.

Límites de tasa: un token bucket y tres cabeceras#

Elido mide la API con un token bucket, con alcance por espacio de trabajo. Las tasas sostenidas publicadas son 10 solicitudes por segundo en Free, 100 en Pro, 500 en Business, y un techo negociado en Enterprise. Pro tiene una capacidad de burst de 200, lo que significa que un bucket lleno te permite enviar 200 solicitudes a la vez antes de que la tasa vuelva a las 100 sostenidas por segundo. La mayoría de los trabajos de creación de enlaces caben en el burst y nunca sienten el límite.

No tienes que adivinar dónde estás. Cada respuesta lleva tres cabeceras:

  • X-RateLimit-Limit - el techo actual por segundo.
  • X-RateLimit-Remaining - tokens restantes en la ventana actual.
  • X-RateLimit-Reset - el timestamp Unix en que el bucket se recarga.

Un cliente bien comportado lee X-RateLimit-Remaining y reduce la velocidad antes de llegar a cero, en lugar de correr hacia una pared de 429 y reaccionar después del hecho. El ritmo proactivo mantiene el throughput fluido; reintentar reactivamente después de cada rechazo malgasta viajes de ida y vuelta y, si todos los clientes reintentan en el mismo instante, genera una estampida.

Un token bucket recargándose a la tasa del espacio de trabajo mientras las solicitudes consumen tokens, con las tres cabeceras de respuesta de límite de tasa mostradas, y un 429 devuelto cuando el bucket se vacía

Cuando realmente necesitas crear miles de enlaces, no hagas un bucle sobre el endpoint de creación individual. POST /v1/links/bulk acepta hasta 1000 enlaces en una sola solicitud y cuenta como una sola unidad contra el límite de tasa. Una llamada masiva mueve mil enlaces al coste de un token; mil llamadas individuales queman mil tokens y la mayor parte de tu burst. La ruta masiva es como la importación desde Google Sheets mueve los enlaces de una campaña entera sin activar el limitador.

Un 429 Too Many Requests - el estado que RFC 6585 reserva exactamente para esto - vuelve con un valor retry_after que indica cuántos segundos esperar. Respétalo. Ese número es el limitador diciéndote exactamente cuándo habrá un token disponible, que es mejor información que cualquier estimación que produciría tu backoff.

Reintentos: qué códigos y cómo hacer backoff#

No todos los errores valen la pena reintentar, y reintentar el equivocado es cómo un fallo pequeño se convierte en una interrupción. Clasifica las respuestas en dos grupos.

Reintenta estos, porque son transitorios: 429 (fuiste demasiado rápido), y 500, 502, 503, 504 (un fallo del servidor o de la pasarela que puede resolverse por sí solo). No reintentes estos, porque la misma solicitud fallará igual: 400 (el payload no es válido), 401 (el token falta o es incorrecto), 403 (al token le falta el scope), 404 (el recurso no existe o no es tuyo), y 409 (conflicto de slug o una edición con versión obsoleta). El primer grupo es "espera e inténtalo de nuevo." El segundo es "arregla el código o la entrada." Reintentar un 400 en un bucle cerrado convierte un bug en un ataque de denegación de servicio contra ti mismo.

Para los códigos reintentables, el algoritmo que importa es el backoff exponencial con jitter. El backoff exponencial simple - duplicar la espera en cada intento - sigue sincronizando los clientes, porque todos los que fallaron en el mismo momento también reintentan en los mismos momentos. Añadir aleatoriedad los dispersa. El artículo de AWS sobre backoff exponencial y jitter es la referencia canónica y muestra por qué la versión con jitter reduce drásticamente la contención. Una versión compacta en 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++;
  }
}

Tres cosas hacen que esto sea seguro en lugar de peligroso. Limita los intentos, para que un fallo persistente falle visiblemente en lugar de girar para siempre. Respeta Retry-After cuando el servidor lo envía, recurriendo al backoff calculado solo cuando no lo hace. Y añade jitter, para que un grupo de workers recuperándose del mismo pico no estampe al unísono. Los SDK oficiales implementan esta misma política de serie - @elido/sdk, elido-python y el cliente Go reintentan exactamente los cinco códigos transitorios con backoff con jitter - que es la razón principal para usar un SDK en lugar de un cliente HTTP hecho a mano.

Hay una regla que conecta los reintentos con la siguiente sección: un reintento de una creación solo es seguro si la creación es idempotente. De lo contrario, cada reintento arriesga crear un segundo enlace.

Idempotencia: cómo no crear enlaces duplicados#

El fallo clásico es así. Tu worker crea un enlace corto, el enlace se crea, pero el 200 nunca llega de vuelta - la conexión se cae en el viaje de regreso. El worker ve un timeout, asume un fallo y reintenta. Ahora tienes dos enlaces para una campaña. A escala, el panel se llena de /foo, /foo-1, /foo-2, y los duplicados sesgán todos los informes downstream.

Las claves de idempotencia cierran esa brecha. Envía una cabecera Idempotency-Key en una solicitud mutante - cualquier cadena de hasta 255 caracteres - y el servidor almacena la respuesta contra ella. Presenta la misma clave de nuevo y obtienes la respuesta original, código de estado y cuerpo, sin que la operación se ejecute dos veces. El patrón es el mismo que Stripe documenta para solicitudes idempotentes, y es la forma estándar de hacer que una red poco fiable sea segura para las escrituras.

El detalle que lo hace funcionar o fracasar es de dónde viene la clave. No generes una clave aleatoria por intento - eso anula el propósito, porque entonces cada reintento parece una operación nueva. Derívala de un identificador de negocio estable para que la misma acción lógica produzca siempre la misma clave:

const link = await elido.links.create(
  { destinationUrl: order.landingUrl },
  { idempotencyKey: `order-${order.id}-link` },
);

Ahora un reintento del mismo trabajo lleva order-12345-link de nuevo, encuentra la respuesta almacenada y devuelve el enlace que ya existe. Exactamente un enlace por pedido, sin importar cuántas veces la cola lo reentregue. Esto es lo que te permite combinar el bucle de backoff anterior con creaciones de forma segura: el reintento y la clave de idempotencia son dos mitades de la misma garantía.

Una cola de trabajos at-least-once disparando dos solicitudes de creación con la misma clave de idempotencia; el servidor almacena la primera respuesta y la devuelve para la segunda, de modo que se crea exactamente un enlace

Dos límites a tener en cuenta. La clave tiene alcance por espacio de trabajo: la misma clave en dos espacios de trabajo crea dos enlaces, lo cual es correcto para una API multi-tenant pero sorprende a los equipos que asumen que las claves son globales. Y la caché no es permanente - en Elido dura 24 horas con clave en (workspace, key). Un reintento dentro de la ventana deduplica; un reintento tres días después, de un trabajo bloqueado que finalmente se procesó, creará un enlace nuevo. Para lotes de varios días, no dependas solo de la clave. Persiste el ID del enlace devuelto en el primer éxito y consúltalo antes de volver a emitir la creación. La IETF ha estado estandarizando esta cabecera en el borrador de Idempotency-Key, y la advertencia de la ventana de 24 horas también se menciona allí.

Si estás conectando una integración de API hoy y quieres que sobreviva a sus propios reintentos, empieza con un espacio de trabajo gratuito, genera un token de cuenta de servicio, y pon una clave de idempotencia en tu primera creación en lugar de añadirla después de que aparezcan los duplicados.

Uniendo todo#

Una llamada de creación lista para producción es los tres mecanismos apilados. Ajusta el ritmo según las cabeceras de límite de tasa para que rara vez alcances un 429. Envuelve la llamada en un backoff con jitter que reintente solo los códigos transitorios y respete Retry-After. Lleva una clave de idempotencia derivada de un ID de negocio para que el reintento sea seguro. Con el SDK oficial, los dos primeros vienen de serie y tú solo proporcionas la clave:

import { Elido, ElidoRateLimitError } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

export async function shortenForOrder(order: Order) {
  try {
    return await elido.links.create(
      { destinationUrl: order.landingUrl, tags: [`order:${order.id}`] },
      { idempotencyKey: `order-${order.id}-link` },
    );
  } catch (err) {
    if (err instanceof ElidoRateLimitError) {
      // SDK already retried with backoff; we are still limited. Defer the job.
      throw new RetryableJobError(err.retryAfter);
    }
    throw err; // non-retryable: surface it
  }
}

Nada de esto es exótico. Es la misma disciplina que merece cualquier API con muchas escrituras, aplicada a enlaces. La recompensa es una integración que hace lo correcto bajo carga en lugar de corromper silenciosamente tu inventario de enlaces. Para el lado de lectura de la misma API - extraer datos de clics sin azotar el limitador - los compromisos están en webhooks versus polling para el rastreo de clics, y la superficie completa de endpoints está en la página de API y SDKs y el resumen de soluciones para desarrolladores.

Más en el blog#

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Gratis, sin registro · 2 por día

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

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

Seguir leyendo