Коротка відповідь
Обмежуйте ресурс: БД, зовнішню quota, CPU, bandwidth, пошту чи jobs. Ключ відповідає користувачу чи tenant, а перевірка атомарна між інстансами. Локальний counter демонструє алгоритм, не спільний production-limit workers чи serverless.
Поєднайте частоту, межі вводу, concurrency й вартість. Десять читань і десять експортів різні. Повертайте 429 для quota, корисний Retry-After та визначте відмову store limiter.
Практичні кроки реалізації
Визначте дорогі операції
Перелічіть до й після входу: login, uploads, search, exports, paid providers. Обмежте body, pages, filters і час до роботи. За багатьох items потрібна вартість маршруту. Один requests/minute не описує всі потреби.
Спроєктуйте ідентичність і scope
User чи tenant підходить account-quota, окремо — pre-auth abuse. IP групує shared-network і залежить від proxy. Forwarding довіряйте лише від відомого proxy, який переписує attacker-ввід. Клієнт не вибирає quota-key чи tenant.
Виконуйте атомарно й спільно
Перевірка й окремий increment мають race. Store-операція чи transaction споживає атомарно. Визначте expiry й cardinality, щоб storage не зростав без меж. Database-locks потребують оцінки contention, інакше limiter сам стане bottleneck.
Визначте outage й повтор
Fail-open зберігає доступність, але відкриває дорогі операції; fail-closed відхиляє клієнтів. Рішення залежить від операції й моніториться. Повтори обмежені з jitter, не одночасні. Concurrency має finally-release і відновлення abandoned-work.
Приклад реалізації
Helper формує відмову після рішення справжнього limiter. Він не реалізує distributed-limit. Передайте час із авторитетного store, не стороннього counter.
export function quotaExceeded(retryAtMs: number, nowMs = Date.now()): Response {
if (!Number.isFinite(retryAtMs)) throw new Error("Invalid retry time");
const seconds = Math.max(1, Math.ceil((retryAtMs - nowMs) / 1000));
return Response.json(
{ code: "RATE_LIMITED", message: "Please retry later" },
{
status: 429,
headers: {
"Retry-After": String(seconds),
"Cache-Control": "no-store",
},
},
);
}
429 позначає quota, не успішний порожній результат. Загальний outage може потребувати іншого статусу. Не розкривайте чужі ID, дайте retry-state. Логуйте категорії без credentials і повного payload.
Компроміси та альтернативи
Fixed-window простий, але має bursts на межах. Sliding згладжує ціною storage чи CPU. Token-capacity підтримує bursts з атомарним обліком. Жоден не замінює concurrency чи payload. Вибирайте за ресурсом і перевіряйте одночасність.
Поширені помилки
- Memory-counter як shared-limit.
- Довільний X-Forwarded-For.
- Quota читання для exports.
- Окремі check й increment.
- Невизначені outage і abandoned-slots.
Контрольний список перед випуском
- Ресурси й вартість визначено.
- Identity з довіреного стану.
- Shared-limit атомарний.
- Payload, concurrency і timeout доповнюють quota.
- Outage описаний і видимий.
- 429 і повтори перевірено.
Поширені запитання
Чи rate-limit вистачає проти abuse?
Ні. Потрібні вхід, ownership, межі, timeout та спеціальні controls. Дозволений запит може бути забороненим чи дорогим.
Чи всім потрібна одна затримка?
Затримка походить із контракту. Розрізняйте burst, daily-quota й disabled-account. Не обіцяйте недійсний retry-time.
Висновок
Захистіть bottleneck scoped-атомарним обліком і межами. Перевірте concurrency й outage, дайте відновлювану відмову. Requests/minute — лише частина дизайну.
Первинні джерела
Пов’язані посібники та приклади
Потрібно реалізувати це у вашому продукті?
Перегляньте відповідні приклади продуктів або поділіться контекстом розробки вебзастосунку, розширення Chrome, API чи SaaS.