8 min di letturaIngegneria

API per accorciatori di URL: rate limit, retry e idempotenza

Come chiamare in produzione l'API di un URL shortener: rate limit token bucket, quali stati riprovare con backoff e chiavi di idempotenza contro i duplicati.

Marius Voß
DevRel · edge infra

Tre endpoint, un header di autenticazione, un corpo JSON. Un'API per accorciatori di URL è una delle integrazioni più semplici di qualsiasi backlog, e il quickstart ti porta a un link abbreviato funzionante in pochi minuti. Quello che il quickstart salta è tutto ciò che accade quando l'integrazione gira a volume: il rate limiter che rispedisce indietro, un 503 transitorio a metà batch, una coda di job che consegna lo stesso messaggio due volte. Sbagliare questi punti significa link duplicati, lavoro perso e una tempesta di 429 che peggiora le cose.

Questo articolo è il complemento di hardening in produzione al quickstart API. Copre i tre meccanismi che separano una demo da un'integrazione affidabile: i rate limit e come regolarsi su di essi, quali errori riprovare e come fare backoff, e le chiavi di idempotenza che impediscono a un retry di creare un secondo link. Gli esempi usano l'API di Elido, ma i pattern sono gli stessi con qualsiasi API per link shortener ben costruita. Se tratti i link abbreviati come infrastruttura gestita dal codice, il caso più ampio per questo approccio è in link abbreviati come Terraform.

Rate limit: un token bucket e un header Retry-After

Elido regola l'API con un token bucket, con scope per workspace. I tassi sostenuti sono 10 richieste al secondo su Free, 30 su Pro e 100 su Business, con capacità di burst di 20, 60 e 200. Su Pro, per esempio, un bucket pieno ti lascia lanciare 60 richieste in un colpo prima che il tasso torni ai 30 al secondo sostenuti. Sopra c'è un bucket separato per chiave API, così una chiave rumorosa non si mangia l'intero workspace. La maggior parte dei job di creazione link sta nel burst e non sente mai il limite.

Non devi indovinare quando scatta un limite. Una richiesta rifiutata porta tre segnali:

  • Retry-After - quanti secondi attendere prima di riprovare (attualmente 1).
  • X-RateLimit-Scope - quale bucket è scattato: workspace o api_key.
  • Il body JSON - {"error":"workspace rate limit exceeded"}, che nomina il limite.

Non ci sono header con i token rimasti, quindi regola il ritmo lato client: tieni la concorrenza pari o sotto il tasso sostenuto del tuo piano e al primo 429 fermati e aspetta Retry-After, invece di correre contro un muro di rifiuti. Un ritmo anticipato mantiene il throughput fluido; riprovare alla cieca dopo ogni rifiuto spreca round trip e, se ogni client riprova nello stesso istante, crea una thundering herd.

Un token bucket che si ricarica al tasso del workspace mentre le richieste consumano token, e una risposta 429 con header Retry-After quando il bucket si svuota

Quando devi creare molti link, non fare un loop sull'endpoint di creazione singola. POST /v1/workspaces/{workspace_id}/links/bulk accetta fino a 100 link in una richiesta. Una chiamata bulk sposta cento link in un solo round trip, mentre cento chiamate singole spenderebbero cento token e gran parte del burst. È così che l'import da Google Sheets sposta i link di una campagna senza far scattare il limiter.

Un 429 Too Many Requests - lo stato che la RFC 6585 riserva esattamente a questo - torna con un header Retry-After che dice quanti secondi attendere. Rispettalo. Quel numero è il limiter che ti dice quando un token sarà disponibile, un'informazione migliore di qualsiasi stima del tuo backoff.

Retry: quali codici e come fare backoff

Non ogni errore vale la pena di essere riprovato, e riprovare quello sbagliato è come un piccolo guasto diventa un'interruzione. Dividi le risposte in due gruppi.

Riprova questi, perché sono transitori: 429 (sei andato troppo veloce), e 500, 502, 503, 504 (un fault lato server o gateway che potrebbe risolversi da solo). Non riprovare questi, perché la stessa richiesta fallirà in modo identico: 400 (il payload non è valido), 401 (il token manca o è sbagliato), 403 (il token non ha lo scope), 404 (la risorsa non esiste o non è tua), e 409 (un conflitto di slug o una modifica con versione obsoleta). Il primo gruppo è "aspetta e riprova." Il secondo è "correggi il codice o l'input." Riprovare un 400 in un loop stretto trasforma un bug in un attacco denial-of-service contro te stesso.

Per i codici riprovabili, l'algoritmo che conta è il backoff esponenziale con jitter. Il backoff esponenziale puro - raddoppia l'attesa a ogni tentativo - sincronizza comunque i client, perché ogni client che ha fallito nello stesso momento riprova anche negli stessi momenti. Aggiungere casualità li distribuisce. Il post di AWS su exponential backoff and jitter è il riferimento canonico e mostra perché la versione con jitter riduce drasticamente la contesa. Una versione compatta in 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++;
  }
}

Tre cose rendono questo codice sicuro invece che pericoloso. Limita i tentativi, così un fault persistente fallisce in modo evidente invece di girare all'infinito. Rispetta Retry-After quando il server lo invia, ricorrendo al backoff calcolato solo quando non lo fa. E usa il jitter, così una flotta di worker che si riprendono dallo stesso problema non si precipita in modo sincronizzato. È pubblicato solo l'SDK Go; gli SDK TypeScript, Python e Ruby non sono ancora nei rispettivi registri, quindi in quei linguaggi questo piccolo wrapper sopra HTTP semplice è l'intero client che ti serve.

C'è una regola che collega i retry alla sezione successiva: un retry di un create è sicuro solo se il create è idempotente. Altrimenti ogni retry rischia di creare un secondo link.

Il fallimento classico si presenta così. Il tuo worker crea un link abbreviato, il link viene creato, ma il 200 non torna mai - la connessione cade durante il viaggio di ritorno. Il worker vede un timeout, assume un fallimento e riprova. Ora hai due link per una campagna. Su larga scala, la dashboard si riempie di /foo, /foo-1, /foo-2, e i duplicati distorcono ogni report a valle.

Le chiavi di idempotenza chiudono quel gap. Invia un header Idempotency-Key su una richiesta mutante - qualsiasi stringa fino a 255 caratteri - e il server memorizza la risposta associata ad essa. Presenta la stessa chiave di nuovo e ricevi la risposta originale, codice di stato e body, senza che l'operazione venga eseguita due volte. Il pattern è lo stesso che Stripe documenta per le richieste idempotenti, ed è il modo standard per rendere una rete inaffidabile sicura per le scritture.

Il dettaglio che determina il successo o il fallimento è da dove viene la chiave. Non generare una chiave casuale per ogni tentativo - questo vanifica lo scopo, perché ogni retry sembrerebbe una nuova operazione. Derivala da un identificatore di business stabile così la stessa azione logica produce sempre la stessa chiave:

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();

Ora un retry dello stesso job porta di nuovo order-12345-link, trova la risposta memorizzata e restituisce il link già esistente. Esattamente un link per ordine, indipendentemente da quante volte la coda riconsegna. Questo è ciò che ti permette di combinare in modo sicuro il loop di backoff precedente con i create: il retry e la chiave di idempotenza sono due metà della stessa garanzia.

Una coda di job at-least-once che invia due richieste di create con la stessa chiave di idempotenza; il server memorizza la prima risposta e la restituisce per la seconda, così viene creato esattamente un link

Due limiti da tenere a mente. La chiave ha scope per workspace: la stessa chiave in due workspace crea due link, il che è corretto per un'API multi-tenant ma sorprende i team che assumono che le chiavi siano globali. E la cache non dura per sempre - su Elido tiene per 24 ore con chiave su (workspace, key). Un retry entro la finestra deduplica; un retry tre giorni dopo, da un job bloccato che finalmente si svuota, creerà un link nuovo. Per i batch multi-giorno, non fare affidamento solo sulla chiave. Persisti l'ID del link restituito dal primo successo e cerca quello prima di riemettere il create. L'IETF sta standardizzando questo header nella bozza Idempotency-Key, e la limitazione della finestra di 24 ore è citata anche lì.

Se stai cablando un'integrazione API oggi e vuoi che sopravviva ai propri retry, inizia su un workspace gratuito, genera un token di service account e metti una chiave di idempotenza sul tuo primissimo create invece di aggiungerla dopo che i duplicati compaiono.

Mettere tutto insieme

Una chiamata di create di livello produzione è i tre meccanismi impilati. Regola il ritmo del client sul tasso sostenuto del tuo piano così raramente colpisci 429. Avvolgi la chiamata in un backoff con jitter che riprova solo i codici transitori e rispetta Retry-After. Porta una chiave di idempotenza derivata da un ID di business così il retry è sicuro. Con fetch semplice, il wrapper qui sopra copre i primi due e tu fornisci solo la chiave:

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();
}

Niente di tutto questo è esotico. È la stessa disciplina che merita qualsiasi API ad alto volume di scritture, applicata ai link. La ricompensa è un'integrazione che fa la cosa giusta sotto carico invece di corrompere silenziosamente il tuo inventario di link. Per il lato di lettura della stessa API - estrarre i dati sui clic senza martellare il limiter - i compromessi sono in webhook vs polling per il tracciamento dei clic, e la superficie completa degli endpoint vive sulla pagina API e SDK e nella panoramica delle soluzioni per sviluppatori.

Articoli correlati sul blog

Domande frequenti

Quali sono i rate limit su un'API per abbreviatori di URL?

Su Elido i limiti sono per workspace e dipendono dal piano, con un token bucket: 10 richieste al secondo su Free (burst 20), 30 su Pro (burst 60) e 100 su Business (burst 200). Sopra c'è un bucket separato per ogni chiave API. Quando un limite scatta, l'API risponde 429 con gli header Retry-After e X-RateLimit-Scope, che indica quale bucket è stato colpito, così il client sa quanto attendere. Le creazioni in blocco accettano fino a 100 link per richiesta, quindi l'endpoint bulk è lo strumento giusto per i lavori grandi.

Quali errori API dovrei riprovare?

Riprova solo i fault transitori: 429 (rate limited), 500, 502, 503 e 504. Usa il backoff esponenziale con jitter e un limite massimo di tentativi. Non riprovare mai 400, 401, 403, 404 o 409 - sono fallimenti di validazione, problemi di autenticazione o conflitti, e la stessa richiesta fallirà di nuovo. Abbina i retry a una chiave di idempotenza così un create riprovato non produce un link duplicato.

Cos'è una chiave di idempotenza e quando serve?

Una chiave di idempotenza è una stringa opaca che si invia in una richiesta mutante affinché il server possa riconoscere un retry e restituire il risultato originale invece di agire due volte. Serve ogni volta che un create o un update può essere riprovato - il che vale sempre, perché le reti falliscono e le code di job consegnano almeno una volta. Deriva la chiave da un identificatore di business stabile come un ID ordine, così la stessa operazione logica porta sempre la stessa chiave.

Come evito di creare link abbreviati duplicati?

Invia una chiave di idempotenza derivata da un identificatore di business stabile su ogni create. Se la chiamata viene riprovata, il server restituisce il link creato originariamente invece di crearne un secondo. Per i job che possono riprovare giorni dopo - oltre la finestra della cache di idempotenza - salva anche l'ID del link restituito dal primo successo e cerca quello prima di riemettere il create.

Quanto dura una chiave di idempotenza?

Su Elido la cache di idempotenza dura 24 ore, con chiave sul workspace più la key. Un retry entro quella finestra restituisce la risposta memorizzata; un retry dopo la finestra viene trattato come una nuova richiesta e creerà una nuova risorsa. Per i batch multi-giorno, persisti l'ID del link risultante al primo successo invece di affidarti soltanto alla chiave.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Senza registrazione. Crea un account gratuito per 50 link al mese.

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

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

Continua a leggere