Пошук уроків, статей та іншого контенту
Обмежите частоту запитів до API та сторінок, використовуючи відповідну стратегію й сховище лімітів.
Rate limiting — це обмеження кількості запитів, які клієнт може виконати за певний проміжок часу.
Наприклад:
не більше 10 запитів до API за 10 секунд;
не більше 5 спроб входу за хвилину;
не більше 100 переглядів сторінок за хвилину для однієї IP-адреси.
Rate limiting потрібен для:
захисту API від перевантаження;
обмеження brute-force атак;
контролю вартості зовнішніх API;
захисту дорогих операцій;
зменшення впливу ботів і автоматизованих клієнтів.
Коли ліміт перевищено, сервер зазвичай повертає статус:
429 Too Many RequestsРазом із відповіддю бажано передавати заголовок Retry-After, який повідомляє клієнту, через скільки секунд можна повторити запит.
Фіксоване вікно розділяє час на інтервали однакової довжини.
Наприклад:
10 запитів з 12:00:00 до 12:00:09;
наступні 10 запитів з 12:00:10 до 12:00:19.
Перевага — простота реалізації. Недолік — клієнт може виконати багато запитів на межі двох вікон:
12:00:09 — 10 запитів
12:00:10 — ще 10 запитівКовзне вікно враховує запити за останні N секунд від поточного моменту.
Наприклад, ліміт 10 запитів за 10 секунд перевіряє інтервал:
поточний час - 10 секунд ... поточний часЦе точніша стратегія, але вона потребує складнішого зберігання стану.
Клієнт отримує «відро» токенів. Кожен запит витрачає один токен, а токени поступово відновлюються.
Ця стратегія дозволяє короткочасні сплески навантаження, зберігаючи середню швидкість запитів у межах ліміту.
Вибір стратегії:
fixed window — для простих і недорогих обмежень;
sliding window — для точнішого контролю;
token bucket — коли потрібно дозволити контрольовані короткі сплески.
Rate limiter має зберігати стан між запитами. Можливі варіанти:
Наприклад, Map у модулі Node.js.
Це підходить лише для локальної розробки. У production такий підхід ненадійний, оскільки:
serverless-функції можуть запускатися в різних процесах;
кожен instance матиме власний лічильник;
процес може бути перезапущений;
кілька регіонів не матимуть спільного стану;
горизонтальне масштабування зламає загальний ліміт.
Для production потрібне сховище, доступне всім instance застосунку. Redis добре підходить, оскільки підтримує атомарні операції та має малу затримку.
Для Next.js, включно з Edge Runtime, зручно використовувати Redis через HTTP. У цьому прикладі використано:
@upstash/redis — клієнт Redis;
@upstash/ratelimit — готові алгоритми rate limiting.
Встановлення залежностей:
npm install @upstash/redis @upstash/ratelimitУ змінних середовища потрібно вказати:
UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-tokenНе додавайте ці значення до клієнтського коду або змінних із префіксом NEXT_PUBLIC_.
Створимо окремий rate limiter для API. Один ідентифікатор отримає не більше 10 запитів за 10 секунд.
Файл src/lib/rate-limit.ts:
import { Redis } from "@upstash/redis";
import { Ratelimit } from "@upstash/ratelimit";
const redis = Redis.fromEnv();
export const apiRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "10 s"),
prefix: "ratelimit:api",
analytics: true,
});prefix додає префікс до ключів у Redis. Це дозволяє розділити ліміти для різних частин застосунку.
Наприклад, для автентифікованого користувача ідентифікатором має бути його стабільний userId. Для неавторизованого клієнта можна використати IP-адресу.
Файл src/lib/request-identity.ts:
import type { NextRequest } from "next/server";
export function getClientIp(request: NextRequest): string {
const forwardedFor = request.headers.get("x-forwarded-for");
if (forwardedFor) {
return forwardedFor.split(",")[0].trim();
}
return request.headers.get("x-real-ip") ?? "unknown";
}Заголовок x-forwarded-for потрібно використовувати лише тоді, коли він додається довіреним reverse proxy або хостинг-провайдером. Якщо застосунок доступний напряму з інтернету, клієнт може підробити цей заголовок.
Тепер використаємо limiter у Route Handler.
Файл src/app/api/search/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { apiRateLimit } from "@/lib/rate-limit";
import { getClientIp } from "@/lib/request-identity";
export async function GET(request: NextRequest) {
const userId = request.headers.get("x-user-id");
const identity = userId ?? getClientIp(request);
const result = await apiRateLimit.limit(identity);
const rateLimitHeaders = {
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": String(Math.max(0, result.remaining)),
"X-RateLimit-Reset": String(result.reset),
};
if (!result.success) {
const retryAfter = Math.max(
1,
Math.ceil((result.reset - Date.now()) / 1000),
);
return NextResponse.json(
{
error: "Забагато запитів",
message: "Повторіть спробу пізніше",
},
{
status: 429,
headers: {
...rateLimitHeaders,
"Retry-After": String(retryAfter),
},
},
);
}
const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
return NextResponse.json(
{
query,
items: [],
},
{
headers: rateLimitHeaders,
},
);
}У цьому прикладі:
авторизований користувач ідентифікується через userId;
для неавторизованого запиту використовується IP;
успішна відповідь містить інформацію про залишок ліміту;
відповідь 429 містить Retry-After;
ліміти є атомарними, оскільки стан зберігається у Redis.
Заголовок
x-user-idу прикладі показує лише місце, де має бути результат вашої автентифікації. Не довіряйте цьому заголовку, якщо клієнт може встановити його самостійно.
У реальному застосунку userId потрібно отримувати із захищеної сесії, JWT або іншого механізму автентифікації.
Route Handler захищає API, але HTML-сторінки також можуть генерувати навантаження. Для їх обмеження можна використати Middleware.
Важливо не застосовувати один і той самий ліміт одночасно в Middleware і Route Handler. Інакше один API-запит може зменшити ліміт двічі.
У цьому прикладі:
API обмежується у власному Route Handler;
сторінки обмежуються у Middleware;
шляхи /api виключені з Middleware.
Файл src/lib/page-rate-limit.ts:
import { Redis } from "@upstash/redis";
import { Ratelimit } from "@upstash/ratelimit";
const redis = Redis.fromEnv();
export const pageRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(60, "1 m"),
prefix: "ratelimit:page",
analytics: true,
});Файл src/middleware.ts:
import { NextRequest, NextResponse } from "next/server";
import { pageRateLimit } from "@/lib/page-rate-limit";
import { getClientIp } from "@/lib/request-identity";
export const config = {
matcher: [
/*
* Не обробляємо API, статичні файли та внутрішні ресурси Next.js.
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
export async function middleware(request: NextRequest) {
const identity = getClientIp(request);
const result = await pageRateLimit.limit(identity);
const headers = new Headers({
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": String(Math.max(0, result.remaining)),
"X-RateLimit-Reset": String(result.reset),
});
if (!result.success) {
const retryAfter = Math.max(
1,
Math.ceil((result.reset - Date.now()) / 1000),
);
headers.set("Retry-After", String(retryAfter));
return new NextResponse(
JSON.stringify({
error: "Забагато запитів",
message: "Сторінка тимчасово недоступна. Повторіть спробу пізніше.",
}),
{
status: 429,
headers: {
...Object.fromEntries(headers),
"Content-Type": "application/json; charset=utf-8",
},
},
);
}
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", String(result.limit));
response.headers.set(
"X-RateLimit-Remaining",
String(Math.max(0, result.remaining)),
);
response.headers.set("X-RateLimit-Reset", String(result.reset));
return response;
}Middleware виконується до обробки сторінки. Якщо ліміт перевищено, рендеринг сторінки не запускається.
Ліміт потрібно застосовувати не просто до запиту, а до певного ключа.
Типові ключі:
user:${userId} — для авторизованих користувачів;
ip:${ip} — для неавторизованих клієнтів;
api-key:${keyId} — для клієнтів із API-ключами;
комбінація користувача та ресурсу — для дорогих операцій.
Наприклад:
const identity = userId
? `user:${userId}`
: `ip:${getClientIp(request)}`;Не варто використовувати лише IP для всіх сценаріїв:
багато користувачів можуть мати одну публічну IP-адресу;
мобільні оператори використовують NAT;
корпоративні мережі можуть об’єднувати сотні клієнтів.
Водночас не варто покладатися лише на userId для неавторизованих endpoint’ів. Інакше один клієнт зможе створювати необмежену кількість анонімних сесій.
Однаковий ліміт для всіх endpoint’ів зазвичай не підходить.
Наприклад:
читання списку товарів — 60 запитів за хвилину;
пошук — 30 запитів за хвилину;
відправлення email — 3 запити за годину;
спроби входу — 5 запитів за хвилину;
створення замовлення — окремий ліміт на користувача.
Для цього створюють окремі limiter’и або різні префікси ключів:
const limiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, "1 m"),
prefix: "ratelimit:auth-login",
});Обмежуйте саме дорогу операцію, а не лише весь маршрут. Наприклад, якщо endpoint виконує кілька різних дій, для кожної дії може бути потрібен власний ліміт.
Корисні заголовки:
X-RateLimit-Limit — максимальна кількість запитів;
X-RateLimit-Remaining — залишок запитів;
X-RateLimit-Reset — час скидання ліміту;
Retry-After — кількість секунд до наступної спроби.
Клієнт може використати їх для повторної спроби:
async function fetchWithRateLimit(url) {
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
await new Promise((resolve) => {
setTimeout(resolve, retryAfter * 1000);
});
return fetchWithRateLimit(url);
}
return response;
}На практиці повторні спроби потрібно обмежувати за кількістю та використовувати exponential backoff. Інакше клієнт може створити ще більше навантаження під час перевищення ліміту.
Rate limiter залежить від зовнішнього сховища. Якщо Redis недоступний, потрібно заздалегідь визначити політику.
Запит дозволяється, якщо перевірити ліміт не вдалося.
Переваги:
тимчасова проблема з Redis не блокує всіх користувачів;
підходить для некритичних сторінок.
Недоліки:
під час збою захист фактично вимикається;
endpoint може бути перевантажений.
Запит блокується, якщо перевірка ліміту не вдалася.
Переваги:
безпечніше для входу, платежів і дорогих операцій;
зберігається захист під час збою сховища.
Недоліки:
короткочасна проблема Redis може зробити endpoint недоступним.
Для критичних endpoint’ів можна явно обробити помилку:
try {
const result = await apiRateLimit.limit(identity);
if (!result.success) {
return NextResponse.json(
{ error: "Забагато запитів" },
{ status: 429 },
);
}
} catch (error) {
console.error("Не вдалося перевірити rate limit", error);
return NextResponse.json(
{ error: "Сервіс тимчасово недоступний" },
{ status: 503 },
);
}Політика має бути однаковою для всіх instance застосунку, а помилки rate limiter потрібно моніторити окремо.
Map у productionЛокальна Map не є спільною між процесами та не забезпечує коректний ліміт під час масштабування.
Перевірка кількості запитів у браузері не захищає API. Клієнтський код можна обійти, змінити або взагалі не виконувати.
Rate limiting завжди має перевірятися на сервері.
Не використовуйте без перевірки:
x-user-id
x-role
x-planКлієнт може самостійно надіслати такі заголовки. Ідентичність потрібно отримувати із захищеного механізму автентифікації.
Якщо Middleware обмежує /api, а Route Handler також викликає limiter, один запит може зменшити ліміт двічі.
Розділіть області відповідальності через matcher або застосовуйте лише один перевіряльник.
Ліміт за IP може заблокувати багатьох реальних користувачів із однієї мережі. Для авторизованих запитів зазвичай краще використовувати userId, а IP залишати додатковим захисним ключем.
Retry-AfterКлієнт не знає, коли повторити запит, тому може безперервно надсилати нові спроби.
Два запити можуть мати дуже різну вартість. Пошук, генерація звіту та відправлення email можуть вимагати різних лімітів.
matcherЯкщо Middleware обробляє статичні ресурси, зображення або внутрішні файли Next.js, ліміт швидко витрачатиметься не на ті операції. Перевіряйте, які шляхи реально потрапляють під matcher.
Rate limiting обмежує кількість запитів від одного клієнта за певний час.
Для production не використовуйте пам’ять окремого Node.js-процесу як спільне сховище лімітів.
Redis підходить для розподіленого rate limiting у Next.js.
API можна обмежувати безпосередньо в Route Handler.
Сторінки можна обмежувати через Middleware.
Не застосовуйте один ліміт двічі до того самого запиту.
Для авторизованих клієнтів використовуйте стабільний userId, а для анонімних — IP або інший обмежений ідентифікатор.
Повертайте 429, Retry-After та інформаційні rate-limit заголовки.
Для різних операцій визначайте різні ліміти.
Заздалегідь оберіть політику на випадок недоступності сховища: fail open або fail closed.