Три эндпоинта, заголовок авторизации, JSON-тело. API сокращателя URL - одна из самых простых интеграций в любом бэклоге, и quickstart позволяет получить рабочую короткую ссылку за несколько минут. Что quickstart пропускает - так это всё, что происходит, когда интеграция работает под нагрузкой: rate limiter начинает отклонять запросы, в середине батча прилетает временный 503, очередь задач доставляет одно и то же сообщение дважды. Допустите ошибки здесь - и получите дублирующие ссылки, потерянную работу и шторм из 429, который делает ситуацию ещё хуже.
Эта статья - компаньон по продакшн-закалке к API quickstart. Она охватывает три механики, отделяющие демо от надёжной интеграции: лимиты запросов и способы регулировать темп, какие ошибки повторять и как выполнять backoff, и ключи идемпотентности, которые не дают повторной попытке создать вторую ссылку. Примеры используют API Elido, но паттерны одинаковы для любого грамотно построенного API сокращателя. Если вы относитесь к коротким ссылкам как к инфраструктуре, управляемой кодом, более широкое обоснование этого подхода - в статье короткие ссылки как Terraform.
Лимиты запросов: token bucket и три заголовка#
Elido регулирует API с помощью token bucket, привязанного к воркспейсу. Опубликованные базовые лимиты составляют 10 запросов в секунду для Free, 100 для Pro, 500 для Business и согласованный потолок для Enterprise. Pro располагает burst-ёмкостью в 200 запросов - то есть полный bucket позволяет отправить 200 запросов одновременно, после чего темп возвращается к базовым 100 в секунду. Большинство задач по созданию ссылок укладываются в burst и вообще не ощущают лимита.
Вам не нужно гадать, где вы находитесь. Каждый ответ содержит три заголовка:
X-RateLimit-Limit- текущий потолок в секунду.X-RateLimit-Remaining- оставшиеся токены в текущем окне.X-RateLimit-Reset- Unix-метка времени, когда bucket пополнится.
Хорошо написанный клиент читает X-RateLimit-Remaining и снижает темп до того, как дойдёт до нуля, а не мчится напролом, спотыкается о стену из 429 и реагирует постфактум. Проактивное регулирование обеспечивает ровный throughput; реактивный повтор после каждого отказа тратит round-trip впустую и, если все клиенты повторяют одновременно, порождает thundering herd.
Когда вам реально нужно создать тысячи ссылок, не зацикливайте эндпоинт единичного создания. POST /v1/links/bulk принимает до 1000 ссылок в одном запросе и считается за единицу против лимита. Один bulk-вызов перемещает тысячу ссылок ценой одного токена; тысяча одиночных вызовов сжигают тысячу токенов и почти весь ваш burst. Именно bulk-путь позволяет импорту из Google Sheets перемещать ссылки целой кампании без срабатывания лимитера.
429 Too Many Requests - статус, который RFC 6585 зарезервировал именно для этого случая - возвращается со значением retry_after, указывающим, сколько секунд нужно подождать. Соблюдайте его. Это число - точная подсказка от лимитера, когда появится следующий токен, и она точнее любого значения, которое вычислит ваш backoff.
Повторные попытки: какие коды и как выполнять backoff#
Не каждую ошибку стоит повторять, и повтор неправильной ошибки - это путь от маленького сбоя к аварии. Разделите ответы на две группы.
Повторяйте эти, потому что они временные: 429 (слишком быстро), и 500, 502, 503, 504 (серверная или шлюзовая ошибка, которая может исчезнуть сама). Не повторяйте эти, потому что тот же запрос завершится так же: 400 (payload невалиден), 401 (токен отсутствует или неверен), 403 (у токена нет нужного скоупа), 404 (ресурс не найден или не принадлежит вам), 409 (конфликт slug или редактирование с устаревшей версией). Первая группа - «подождать и попробовать снова». Вторая - «исправить код или входные данные». Повтор 400 в тугом цикле превращает баг в DoS-атаку на себя.
Для повторяемых кодов ключевой алгоритм - экспоненциальный backoff с jitter. Чистый экспоненциальный backoff - удвоение задержки на каждой попытке - всё равно синхронизирует клиентов, потому что все клиенты, упавшие в один момент, повторяют попытки тоже в одни и те же моменты. Добавление случайности разбрасывает их. Материал AWS о экспоненциальном backoff и jitter - каноническая ссылка, показывающая, почему jitter-версия резко снижает конкуренцию. Компактная версия на 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++;
}
}
Три вещи делают это безопасным, а не опасным. Ограничение числа попыток: стойкая ошибка завершается явно, а не крутится бесконечно. Соблюдение Retry-After, когда его присылает сервер, с откатом на вычисляемый backoff только при его отсутствии. И jitter, чтобы группа воркеров, восстанавливающихся после одного сбоя, не бросилась в атаку в одну ногу. Официальные SDK реализуют ту же политику из коробки - @elido/sdk, elido-python и Go-клиент повторяют ровно пять временных кодов с jitter-backoff - и это главная причина предпочесть SDK самодельному HTTP-клиенту.
Есть одно правило, связывающее повторы со следующим разделом: повтор create безопасен только если create идемпотентен. Иначе каждая попытка рискует создать вторую ссылку.
Идемпотентность: как не создать дублирующие ссылки#
Классический сбой выглядит так. Воркер создаёт короткую ссылку, ссылка создана, но 200 не доходит обратно - соединение обрывается на обратном пути. Воркер видит таймаут, считает это провалом и повторяет. Теперь у вас две ссылки для одной кампании. В масштабе дашборд заполняется /foo, /foo-1, /foo-2, и дубли искажают все отчёты ниже по стеку.
Ключи идемпотентности закрывают этот пробел. Отправьте заголовок Idempotency-Key в мутирующем запросе - любую строку до 255 символов - и сервер сохранит ответ под этим ключом. Предъявите тот же ключ снова - получите исходный ответ обратно, с тем же статус-кодом и телом, без повторного выполнения операции. Паттерн тот же, что Stripe документирует для идемпотентных запросов, и это стандартный способ сделать ненадёжную сеть безопасной для записи.
Деталь, от которой зависит всё, - откуда берётся ключ. Не генерируйте случайный ключ на каждую попытку - это бессмысленно, потому что каждый повтор тогда выглядит как новая операция. Формируйте ключ из стабильного бизнес-идентификатора, чтобы одно и то же логическое действие всегда давало один и тот же ключ:
const link = await elido.links.create(
{ destinationUrl: order.landingUrl },
{ idempotencyKey: `order-${order.id}-link` },
);
Теперь повтор той же задачи снова несёт order-12345-link, попадает в сохранённый ответ и возвращает уже существующую ссылку. Ровно одна ссылка на заказ, сколько бы раз очередь ни доставила сообщение. Именно это позволяет безопасно комбинировать приведённый выше backoff-цикл с созданием ссылок: повтор и ключ идемпотентности - две половины одной гарантии.
Две границы, которые нужно помнить. Ключ привязан к воркспейсу: один и тот же ключ в двух разных воркспейсах создаёт две ссылки - это правильно для мультитенантного API, но удивляет команды, считающие ключи глобальными. И кэш не вечен - в Elido он хранится 24 часа с ключом по паре (workspace, key). Повтор в пределах окна выполняет дедупликацию; повтор через три дня, от зависшей задачи, которая наконец дренировала очередь, создаст новую ссылку. Для многодневных батчей не полагайтесь только на ключ. Сохраняйте ID ссылки, возвращённый при первом успехе, и проверяйте его перед повторным вызовом. IETF стандартизирует этот заголовок в черновике Idempotency-Key, и оговорка о 24-часовом окне там тоже упоминается.
Если вы подключаете API-интеграцию сегодня и хотите, чтобы она переживала собственные повторы, начните с бесплатного воркспейса, сгенерируйте сервисный токен и поставьте ключ идемпотентности на самый первый create - лучше, чем добавлять его потом, когда дубли уже появились.
Всё вместе#
Продакшн-готовый вызов create - это три механики в стеке. Регулируйте темп по заголовкам лимита, чтобы 429 был редкостью. Оберните вызов в jitter-backoff, который повторяет только временные коды и соблюдает Retry-After. Несите ключ идемпотентности, основанный на бизнес-ID, чтобы повтор был безопасным. С официальным SDK первые два дают вам бесплатно, а ключ вы поставляете сами:
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
}
}
Ничего экзотического. Это та же дисциплина, которой заслуживает любой API с интенсивной записью, применённая к ссылкам. Награда - интеграция, которая делает правильное под нагрузкой, а не тихо портит ваш инвентарь ссылок. Для read-стороны того же API - как вытягивать данные о кликах, не нагружая лимитер, - компромиссы разобраны в вебхуки против поллинга для трекинга кликов, а полная поверхность эндпоинтов - на странице API и SDK и в обзоре решений для разработчиков.
Другие материалы в блоге#
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день