Пошук уроків, статей та іншого контенту
Реалізуєте обмеження частоти запитів і порівняєте алгоритми token bucket, leaky bucket та fixed window.
Rate limiting — це обмеження кількості запитів, які клієнт може виконати за певний проміжок часу.
Клієнтом може бути:
IP-адреса;
ідентифікатор користувача;
API-ключ;
токен доступу;
комбінація кількох ознак.
Наприклад, сервіс може дозволити не більше 100 запитів на хвилину для одного API-ключа. Якщо клієнт перевищує ліміт, сервер повертає відповідь:
HTTP/1.1 429 Too Many Requests
Retry-After: 12Rate limiting допомагає:
захистити сервіс від випадкових піків навантаження;
зменшити вплив автоматизованих атак;
не допустити виснаження пулу з'єднань, CPU або пам'яті;
розподілити ресурси між клієнтами;
контролювати використання платних або обмежених API.
Rate limiting не замінює автентифікацію, авторизацію чи захист від DDoS. Це один із рівнів захисту сервісу.
Перед вибором алгоритму потрібно визначити кілька параметрів:
Ідентифікатор клієнта
Наприклад, userId, API-ключ або IP-адреса.
Межу
Наприклад, 10 запитів.
Період або швидкість
Наприклад, 10 запитів за хвилину або в середньому 2 запити за секунду.
Поведінку після перевищення
Найчастіше запит відхиляється з кодом 429.
Місце застосування
Обмеження можна реалізувати на API Gateway, reverse proxy або безпосередньо в застосунку.
Для різних endpoint-ів можуть бути різні правила:
авторизація: суворий ліміт проти підбору паролів;
читання даних: вищий ліміт;
створення ресурсів: нижчий ліміт;
дорогі операції: окремий ліміт на користувача.
Fixed window ділить час на фіксовані інтервали.
Наприклад:
вікно триває 60 секунд;
кожному клієнту дозволено 100 запитів у межах вікна;
після початку нового вікна лічильник обнуляється.
Для клієнта зберігаються:
початок поточного вікна;
кількість запитів у ньому.
Псевдологіка:
якщо поточний час вже за межами вікна:
створити нове вікно і встановити count = 0
якщо count >= limit:
відхилити запит
збільшити count
дозволити запитпроста реалізація;
низьке споживання пам'яті;
простий підрахунок;
зручно реалізувати через атомарний лічильник.
Уявімо ліміт 5 запитів на хвилину:
клієнт виконує 5 запитів о 12:00:59;
ще 5 запитів — о 12:01:01.
Формально ліміт не порушено: у кожному вікні було по 5 запитів. Але за дві секунди сервіс отримав 10 запитів.
Це називають burst на межі вікна.
Token bucket моделює контейнер із токенами.
Параметри алгоритму:
capacity — максимальна кількість токенів;
refillRate — швидкість поповнення токенів;
tokens — поточна кількість токенів;
lastRefillAt — час останнього поповнення.
Для одного запиту потрібно витратити один токен.
Якщо токенів немає, запит відхиляється. Токени поступово відновлюються з визначеною швидкістю.
Наприклад:
місткість: 10 токенів;
поповнення: 2 токени за секунду;
клієнт може одразу виконати до 10 запитів;
після цього в середньому дозволено 2 запити за секунду.
Важливо розрізняти:
burst capacity — скільки запитів дозволено виконати одразу;
refill rate — середня швидкість, з якою клієнт може продовжувати запити.
Якщо з моменту останнього поповнення минуло elapsedSeconds, кількість нових токенів дорівнює:
elapsedSeconds × refillRateНова кількість токенів не може перевищувати місткість контейнера:
tokens = min(capacity, tokens + elapsedSeconds × refillRate)Нижче наведено повний приклад HTTP-сервера без сторонніх бібліотек. Для кожної IP-адреси сервер зберігає окремий контейнер токенів.
const http = require('node:http');
const PORT = 3000;
const CAPACITY = 5;
const REFILL_RATE = 1; // Один токен за секунду
const buckets = new Map();
function getBucket(clientId, now) {
let bucket = buckets.get(clientId);
if (!bucket) {
bucket = {
tokens: CAPACITY,
lastRefillAt: now
};
buckets.set(clientId, bucket);
}
return bucket;
}
function tryConsume(clientId) {
const now = Date.now();
const bucket = getBucket(clientId, now);
const elapsedSeconds = (now - bucket.lastRefillAt) / 1000;
bucket.tokens = Math.min(
CAPACITY,
bucket.tokens + elapsedSeconds * REFILL_RATE
);
bucket.lastRefillAt = now;
if (bucket.tokens < 1) {
const secondsUntilNextToken = (1 - bucket.tokens) / REFILL_RATE;
return {
allowed: false,
retryAfterSeconds: Math.ceil(secondsUntilNextToken)
};
}
bucket.tokens -= 1;
return {
allowed: true,
retryAfterSeconds: 0
};
}
const server = http.createServer((request, response) => {
const clientId = request.socket.remoteAddress || 'unknown';
const result = tryConsume(clientId);
if (!result.allowed) {
response.statusCode = 429;
response.setHeader('Content-Type', 'application/json');
response.setHeader(
'Retry-After',
String(result.retryAfterSeconds)
);
response.end(JSON.stringify({
error: 'Too Many Requests',
retryAfter: result.retryAfterSeconds
}));
return;
}
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify({
message: 'Request accepted'
}));
});
server.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});Запустіть сервер:
node server.jsПерші п'ять запитів зазвичай будуть дозволені одразу. Після цього потрібно чекати, поки контейнер поповниться.
У реальному застосунку IP-адреса не завжди є надійним ідентифікатором. Наприклад, багато користувачів можуть перебувати за одним NAT. Для автентифікованих API зазвичай краще використовувати стабільний ідентифікатор користувача або API-ключ.
Leaky bucket можна уявити як чергу з отвором унизу.
Запити додаються в чергу, а обробляються з фіксованою швидкістю. Якщо черга переповнена, новий запит відхиляється.
Наприклад:
розмір черги: 20 запитів;
швидкість обробки: 2 запити за секунду;
короткий burst може потрапити в чергу;
фактична обробка відбувається рівномірно.
У спрощеному вигляді алгоритм працює так:
видалити з черги запити, час обробки яких уже настав
якщо черга заповнена:
відхилити запит
додати запит у чергу
запланувати його обробкузгладжує нерівномірні сплески;
забезпечує стабільну швидкість обробки;
корисний для захисту повільних або дорогих downstream-сервісів.
потрібна черга;
запит може не відхилитися одразу, а чекати;
потрібно контролювати час очікування;
для кожного клієнта може знадобитися додаткова пам'ять.
У деяких реалізаціях під назвою leaky bucket мають на увазі не чергу запитів, а просто обмеження швидкості. Тому під час вибору готового компонента важливо перевірити його точну поведінку: він ставить запити в чергу чи відхиляє їх.
Підходить, коли:
потрібна дуже проста логіка;
допустимі сплески на межі часових вікон;
ліміт прив'язаний до календарного періоду.
Типова поведінка:
запити одразу дозволяються до досягнення ліміту;
після переходу в нове вікно лічильник обнуляється.
Підходить, коли:
потрібно дозволити короткі bursts;
важлива середня швидкість запитів;
потрібно гнучко керувати місткістю burst і швидкістю відновлення.
Типова поведінка:
накопичені токени дозволяють короткий сплеск;
після вичерпання токенів клієнт чекає на поповнення.
Підходить, коли:
обробка має відбуватися рівномірно;
downstream-сервіс не повинен отримувати bursts;
запити можна поставити в обмежену чергу.
Типова поведінка:
запити надходять у чергу;
черга спорожнюється з фіксованою швидкістю;
переповнені запити відхиляються.
Для відхиленого запиту варто використовувати статус 429 Too Many Requests.
Корисні заголовки:
Retry-After — через скільки секунд можна повторити запит;
RateLimit-Limit — встановлений ліміт;
RateLimit-Remaining — приблизна кількість доступних запитів;
RateLimit-Reset — час або кількість секунд до оновлення ліміту.
Приклад відповіді:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 5
RateLimit-Limit: 100
RateLimit-Remaining: 0Тіло відповіді може мати такий вигляд:
{
"error": "rate_limit_exceeded",
"message": "Забагато запитів",
"retryAfter": 5
}Клієнт не повинен повторювати запити в циклі без паузи. Для повторних спроб зазвичай використовують затримку, а для кількох клієнтів — додаткове випадкове зміщення, щоб вони не повторили запити одночасно.
Локальна Map підходить для демонстрації або одного процесу, але має важливі обмеження:
кожен екземпляр сервісу має власний лічильник;
після перезапуску всі дані втрачаються;
клієнт може обходити ліміт, потрапляючи на різні екземпляри;
пам'ять процесу поступово заповнюється записами неактивних клієнтів.
Якщо сервіс працює на кількох екземплярах, стан ліміту має бути спільним або запити повинні стабільно маршрутизуватися до одного екземпляра.
Для спільного стану потрібні операції, які коректно працюють при одночасних запитах:
перевірка і зміна ліміту мають бути атомарними;
час потрібно вимірювати узгоджено;
записи неактивних клієнтів потрібно видаляти або автоматично протерміновувати;
ключі стану мають містити тип ліміту та ідентифікатор клієнта.
Наприклад, різні правила можуть мати різні ключі:
rate-limit:read:user-42
rate-limit:write:user-42
rate-limit:login:ip-203.0.113.10На практиці rate limiting часто розміщують перед застосунком — у gateway або reverse proxy. Це дозволяє відхилити зайві запити до того, як вони витратять ресурси бізнес-логіки.
Не варто вибирати одне число для всіх endpoint-ів. Оцініть:
нормальну частоту запитів клієнта;
час виконання операції;
максимальне допустиме навантаження;
розмір допустимого burst;
поведінку клієнтів після отримання 429.
Для Token Bucket потрібно окремо визначити:
capacity — максимальний burst;
refillRate — стійку середню швидкість.
Наприклад, конфігурація:
capacity = 20
refillRate = 2 токени/секундуозначає, що клієнт може виконати 20 запитів одразу, але після цього середня швидкість становитиме приблизно 2 запити за секунду.
Після впровадження потрібно перевірити:
чи не блокуються нормальні клієнти;
чи справді обмежуються bursts;
чи коректно працює ліміт на кількох екземплярах;
чи не зростає пам'ять через неактивні записи;
чи повертається зрозумілий час повторної спроби.
Якщо рахувати всі запити в одному лічильнику, один активний клієнт може заблокувати всіх інших.
Краще застосовувати ліміт щонайменше за ідентифікатором клієнта. Для дорогих операцій можна додати окремий ліміт на endpoint.
IP-адреса може бути спільною для багатьох користувачів. Також за проксі сервер може бачити адресу самого проксі, а не клієнта.
IP може бути корисним для публічних endpoint-ів, але для автентифікованих запитів часто доречніше використовувати користувача або API-ключ.
Retry-AfterКлієнт не знає, коли повторити запит, і може почати агресивно повторювати його одразу після 429.
Повертайте Retry-After, якщо можете розрахувати час до наступного дозволеного запиту.
Якщо два запити одночасно читають старе значення лічильника, обидва можуть пройти, хоча дозволеним був лише один.
У розподіленій системі перевірка та списання токена мають виконуватися як одна атомарна операція.
Якщо для кожного нового клієнта створюється запис, але старі записи не видаляються, пам'ять поступово заповнюється.
Для стану потрібно передбачити час життя, періодичне очищення або сховище з автоматичним протермінуванням.
Низький ліміт може блокувати нормальну поведінку:
повторне завантаження сторінки;
кілька паралельних запитів інтерфейсу;
повторні спроби після тимчасової помилки.
Ліміт потрібно налаштовувати на основі вимірювань і реального сценарію використання.
Rate limiting обмежує частоту запитів і захищає ресурси сервісу.
Fixed Window простий, але може дозволити великий burst на межі вікон.
Token Bucket підтримує burst і контролює середню швидкість через поповнення токенів.
Leaky Bucket вирівнює потік, використовуючи чергу та фіксовану швидкість обробки.
При перевищенні ліміту слід повертати 429 Too Many Requests.
Заголовок Retry-After допомагає клієнту правильно повторити запит.
У розподіленій системі стан ліміту має бути спільним або запити повинні оброблятися узгоджено одним екземпляром.
Алгоритм і параметри потрібно вибирати окремо для конкретного клієнта, endpoint-у та характеру навантаження.