7 min czytaniaInżynieria

API skracacza URL: limity zapytań, ponowne próby, idempotentność

Jak wywoływać API skracacza URL w produkcji: limity token-bucket, kody statusu do ponowienia z backoffem i klucze idempotentności, które zapobiegają duplikatom.

Marius Voß
DevRel · edge infra
Token bucket mierzący zapytania API, pętla retry z backoffem i klucz idempotentności deduplikujący zduplikowane wywołanie create, w palecie kolorów Elido

Trzy endpointy, nagłówek auth, ciało JSON. API skracacza URL to jedna z łatwiejszych integracji w każdym backlogu, a quickstart daje działający krótki link w kilka minut. To, czego quickstart pomija, to wszystko, co dzieje się gdy integracja działa w dużej skali: rate limiter odpycha, przejściowy 503 w środku batcha, kolejka zadań dostarcza tę samą wiadomość dwa razy. Zrób to źle, a dostaniesz zduplikowane linki, utracone zadania i burzę 429, która pogarsza sprawę.

Ten post jest towarzyszem do quickstartu API dotyczącym hartowania produkcyjnego. Omawia trzy mechaniki, które odróżniają demo od niezawodnej integracji: limity zapytań i jak regulować tempo wobec nich, które błędy ponawiać i jak się cofnąć, oraz klucze idempotentności, które zapobiegają tworzeniu drugiego linku przy ponownej próbie. Przykłady używają API Elido, ale wzorce są takie same w każdym dobrze zbudowanym API skracacza linków. Jeśli traktujesz krótkie linki jako infrastrukturę zarządzaną z kodu, szerszy argument za tym jest w artykule krótkie linki jako Terraform.

Limity Zapytań: Token Bucket i Trzy Nagłówki#

Elido mierzy API za pomocą token bucket, w zakresie workspace'u. Opublikowane trwałe limity to 10 zapytań na sekundę dla planu Free, 100 dla Pro, 500 dla Business i negocjowany sufit dla Enterprise. Pro ma pojemność burst wynoszącą 200, co oznacza że pełny kubełek pozwala wystrzelić 200 zapytań naraz, zanim tempo wróci do trwałych 100 na sekundę. Większość zadań tworzenia linków mieści się w burście i w ogóle nie odczuwa limitu.

Nie musisz zgadywać, gdzie jesteś. Każda odpowiedź zawiera trzy nagłówki:

  • X-RateLimit-Limit - aktualny limit na sekundę.
  • X-RateLimit-Remaining - tokeny pozostałe w bieżącym oknie.
  • X-RateLimit-Reset - znacznik czasu Unix, gdy kubełek się uzupełni.

Dobrze zachowujący się klient czyta X-RateLimit-Remaining i zwalnia zanim dojdzie do zera, zamiast pędzić w ścianę 429 i reagować po fakcie. Proaktywne regulowanie tempa utrzymuje płynną przepustowość; reaktywne ponawianie po każdym odrzuceniu marnuje rundy, a jeśli wszyscy klienci ponawia w tym samym momencie, wytwarza efekt thundering herd.

Token bucket uzupełniający się w tempie workspace'u podczas gdy zapytania pobierają tokeny, z widocznymi trzema nagłówkami odpowiedzi rate-limit, i 429 zwracanym gdy kubełek się opróżni

Gdy naprawdę musisz stworzyć tysiące linków, nie zapętlaj endpointu do tworzenia pojedynczego linku. POST /v1/links/bulk akceptuje do 1000 linków w jednym zapytaniu i liczy się jako jedna jednostka wobec limitu. Jedno wywołanie bulk przenosi tysiąc linków za koszt jednego tokenu; tysiąc pojedynczych wywołań spala tysiąc tokenów i większość twojego burstu. Ścieżka bulk to sposób, w jaki import z Google Sheets przenosi linki warte kampanii bez wyzwalania limitera.

429 Too Many Requests - status zarezerwowany przez RFC 6585 dokładnie do tego celu - wraca z wartością retry_after mówiącą ile sekund czekać. Uszanuj to. Ta liczba to limiter mówiący dokładnie, kiedy token będzie dostępny - co jest lepszą informacją niż jakikolwiek szacunek, który produkowałby twój backoff.

Ponowne Próby: Które Kody i Jak Się Cofnąć#

Nie każdy błąd warto ponawiać, a ponawianie złego to sposób, w jaki mały błąd staje się awarią. Posortuj odpowiedzi na dwie grupy.

Ponawiane - bo są przejściowe: 429 (byłeś za szybki) oraz 500, 502, 503, 504 (błąd po stronie serwera lub bramki, który może sam się rozwiązać). Nie ponawiane - bo to samo zapytanie znowu zakończy się błędem: 400 (ładunek jest nieprawidłowy), 401 (token brakuje lub jest zły), 403 (token nie ma zakresu), 404 (zasób nie istnieje lub nie należy do ciebie), 409 (konflikt slugu lub edycja nieaktualnej wersji). Pierwsza grupa to "poczekaj i spróbuj ponownie". Druga to "napraw kod lub dane wejściowe". Ponawianie 400 w ciasnej pętli zamienia bug w atak denial-of-service na samego siebie.

Dla ponawialnych kodów algorytmem, który ma znaczenie, jest wykładniczy backoff z jitterem. Zwykły wykładniczy backoff - podwajanie oczekiwania przy każdej próbie - nadal synchronizuje klientów, bo każdy klient, który zawiódł w tym samym momencie, ponawia też w tych samych momentach. Dodanie losowości rozkłada je. Artykuł AWS o wykładniczym backoffie i jitterze jest kanonicznym odniesieniem i pokazuje, dlaczego wersja z jitterem dramatycznie zmniejsza rywalizację. Zwięzła wersja w 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++;
  }
}

Trzy rzeczy sprawiają, że to jest bezpieczne, a nie niebezpieczne. Ogranicza liczbę prób, więc trwały błąd kończy się głośnym niepowodzeniem zamiast kręcenia w kółko w nieskończoność. Honoruje Retry-After gdy serwer go wysyła, wracając do obliczonego backoffu tylko gdy tego nie robi. I jest jitterowany, więc flota pracowników odbudowująca się po tym samym zakłóceniu nie atakuje w zsynchronizowanym rytmie. Oficjalne SDK implementują tę samą politykę od razu - @elido/sdk, elido-python i klient Go ponawiają dokładnie pięć przejściowych kodów z jitterowanym backoffem - co jest głównym powodem, by sięgnąć po SDK zamiast ręcznie pisanego klienta HTTP.

Jest jedna reguła wiążąca ponowne próby z następną sekcją: ponowienie create jest bezpieczne tylko jeśli create jest idempotentne. W przeciwnym razie każda ponowna próba ryzykuje powstaniem drugiego linku.

Idempotentność: Jak Nie Tworzyć Zduplikowanych Linków#

Klasyczny błąd wygląda tak. Twój worker tworzy krótki link, link zostaje stworzony, ale 200 nigdy nie dociera z powrotem - połączenie pada w drodze powrotnej. Worker widzi timeout, zakłada błąd i ponawia. Teraz masz dwa linki dla jednej kampanii. W skali dashboard wypełnia się /foo, /foo-1, /foo-2, a duplikaty zakrzywiają każdy raport downstream.

Klucze idempotentności zamykają tę lukę. Wysyłaj nagłówek Idempotency-Key przy mutującym zapytaniu - dowolny ciąg do 255 znaków - a serwer zapisuje odpowiedź pod nim. Prześlij ten sam klucz ponownie i otrzymujesz oryginalną odpowiedź, kod statusu i ciało, bez podwójnego wykonania operacji. Wzorzec jest taki sam jak ten, który Stripe dokumentuje dla idempotentnych zapytań, i jest to standardowy sposób na bezpieczne pisanie przez zawodną sieć.

Szczegół, który decyduje o sukcesie lub porażce, to skąd pochodzi klucz. Nie generuj losowego klucza przy każdej próbie - to niweczy cel, bo każda ponowna próba wygląda wtedy jak nowa operacja. Wyprowadzaj go ze stabilnego identyfikatora biznesowego, żeby ta sama akcja logiczna zawsze dawała ten sam klucz:

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

Teraz ponowna próba tego samego zadania ponownie niesie order-12345-link, trafia w zapisaną odpowiedź i zwraca link, który już istnieje. Dokładnie jeden link na zamówienie, niezależnie od tego, ile razy kolejka dostarcza ponownie. To właśnie pozwala bezpiecznie połączyć pętlę backoffu powyżej z operacjami create: ponowna próba i klucz idempotentności to dwie połowy tej samej gwarancji.

Kolejka zadań at-least-once wysyłająca dwa zapytania create z tym samym kluczem idempotentności; serwer zapisuje pierwszą odpowiedź i zwraca ją przy drugim zapytaniu, tworząc dokładnie jeden link

Dwie granice, o których warto pamiętać. Klucz jest w zakresie workspace'u: ten sam klucz w dwóch workspace'ach tworzy dwa linki, co jest poprawne dla API multi-tenant, ale zaskakuje zespoły, które zakładają że klucze są globalne. I pamięć podręczna nie jest wieczna - w Elido trwa 24 godziny, kluczowana na (workspace, key). Ponowna próba w oknie deduplikuje; ponowna próba trzy dni później, z zablokowanego zadania, które w końcu zostało odblokowane, tworzy nowy link. W przypadku wielodniowych zadań wsadowych nie polegaj wyłącznie na kluczu. Przechowaj ID linku zwróconego przy pierwszym sukcesie i sprawdź go przed ponownym wywołaniem create. IETF standaryzuje ten nagłówek w projekcie Idempotency-Key, a zastrzeżenie dotyczące 24-godzinnego okna jest tam również wspomniane.

Jeśli dziś wpisujesz integrację API i chcesz, żeby przeżyła własne ponowne próby, zacznij na darmowym workspace'ie, wygeneruj token konta serwisowego i dodaj klucz idempotentności już przy pierwszym create, zamiast doprawiać go po tym, jak pojawią się duplikaty.

Składając To Razem#

Produkcyjne wywołanie create to trzy mechaniki ułożone razem. Reguluj tempo wobec nagłówków rate-limit, żeby rzadko trafić w 429. Opakuj wywołanie w jitterowany backoff, który ponawia tylko przejściowe kody i honoruje Retry-After. Noś klucz idempotentności wyprowadzony z ID biznesowego, żeby ponowna próba była bezpieczna. Z oficjalnym SDK pierwsze dwie kwestie masz za darmo, a dostarczasz tylko klucz:

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
  }
}

Nic z tego nie jest egzotyczne. To ta sama dyscyplina, jakiej zasługuje każde API intensywnie zapisujące dane, zastosowana do linków. Nagrodą jest integracja, która robi właściwą rzecz pod obciążeniem zamiast po cichu korumpować twój inwentarz linków. Dla strony odczytującej tego samego API - pobierania danych o kliknięciach bez walenia w limiter - kompromisy są w artykule webhooki kontra polling do śledzenia kliknięć, a pełna powierzchnia endpointów żyje na stronie API i SDK oraz w przeglądzie rozwiązań dla deweloperów.

Powiązane na Blogu#

Wypróbuj Elido

Wklej URL, otrzymaj krótki link

Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.

Za darmo, bez rejestracji · 2 dziennie

Wypróbuj Elido

Skracarka URL hostowana w UE: własne domeny, głęboka analityka i otwarte API. Darmowy plan - bez karty kredytowej.

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

Czytaj dalej