Введение
Rate Limiting (ограничение частоты запросов) — это критически важный механизм защиты API от перегрузки, злоупотребления и DDoS атак. Без rate limiting ваш API может быть перегружен большим количеством запросов от одного клиента, что приведёт к недоступности сервиса для других пользователей.
В этом руководстве мы рассмотрим различные стратегии rate limiting, примеры реализации на популярных языках программирования, использование Redis для распределенного limiting, и best practices для защиты вашего API.
✅ Что вы узнаете:
- ✅ Что такое rate limiting и зачем он нужен
- ✅ 4 стратегии rate limiting (fixed window, sliding window, token bucket, leaky bucket)
- ✅ Реализация на Node.js, Python и Go
- ✅ Использование Redis для распределенного rate limiting
- ✅ HTTP заголовки для информирования клиентов (X-RateLimit-*)
- ✅ Обработка rate limiting на клиенте
- ✅ Best practices и рекомендации
💡 Зачем нужен rate limiting:
- Защита от DDoS: Предотвращение перегрузки сервера
- Справедливое использование: Распределение ресурсов между клиентами
- Контроль затрат: Ограничение использования для экономии ресурсов
- Защита от злоупотребления: Предотвращение автоматических атак
- Улучшение UX: Предотвращение замедления для всех пользователей
📋 Содержание
Что такое Rate Limiting? 🎯
Rate Limiting — это механизм контроля количества запросов, которые клиент может сделать к API за определенный период времени. Когда лимит превышен, API возвращает HTTP статус 429 (Too Many Requests).
Типы лимитов:
- По IP адресу: Ограничение для конкретного IP
- По пользователю: Ограничение для авторизованного пользователя
- По API ключу: Ограничение для конкретного API ключа
- Глобальный: Общее ограничение для всего API
- По endpoint: Разные лимиты для разных endpoints
Примеры лимитов:
- 100 запросов в минуту для обычных пользователей
- 5 попыток входа в 15 минут для авторизации
- 1000 запросов в час для премиум пользователей
- 10 запросов в секунду для конкретного endpoint
Стратегии Rate Limiting 📊
1. Fixed Window (Фиксированное окно)
Fixed Window
Самый простой подход. Время делится на фиксированные интервалы (например, минуты), и каждое окно имеет свой лимит запросов.
Преимущества:
- Простота реализации
- Низкое потребление памяти
- Хорошо работает для простых случаев
Недостатки:
- Burst проблема: все запросы могут быть в начале окна
- Не точное распределение во времени
// Fixed Window пример
const requests = {}; // { userId: { count: 0, resetTime: Date } }
function fixedWindowLimiter(userId, limit, windowMs) {
const now = Date.now();
const user = requests[userId];
// Если окно истекло или не существует
if (!user || now > user.resetTime) {
requests[userId] = {
count: 1,
resetTime: now + windowMs
};
return true; // Разрешить запрос
}
// Если лимит не превышен
if (user.count < limit) {
user.count++;
return true;
}
return false; // Лимит превышен
}
2. Sliding Window (Скользящее окно)
Sliding Window
Окно времени скользит вместе с запросами. Каждый запрос имеет время истечения, и окно постоянно обновляется.
Преимущества:
- Более точное распределение запросов
- Меньше проблем с burst
- Более справедливое распределение
Недостатки:
- Выше потребление памяти
- Сложнее реализация
// Sliding Window пример
const requests = {}; // { userId: [timestamp1, timestamp2, ...] }
function slidingWindowLimiter(userId, limit, windowMs) {
const now = Date.now();
const windowStart = now - windowMs;
if (!requests[userId]) {
requests[userId] = [];
}
// Удалить старые запросы вне окна
requests[userId] = requests[userId].filter(timestamp => timestamp > windowStart);
// Проверить лимит
if (requests[userId].length < limit) {
requests[userId].push(now);
return true;
}
return false;
}
3. Token Bucket (Ведро токенов)
Token Bucket
Токены добавляются в "ведро" с фиксированной скоростью. Каждый запрос потребляет токен. Если токены закончились, запрос блокируется.
Преимущества:
- Позволяет кратковременные burst
- Гибкое управление скоростью
- Хорошо для неравномерной нагрузки
// Token Bucket пример
const buckets = {}; // { userId: { tokens: number, lastRefill: timestamp } }
function tokenBucketLimiter(userId, capacity, refillRate, tokensPerRequest = 1) {
const now = Date.now();
if (!buckets[userId]) {
buckets[userId] = {
tokens: capacity,
lastRefill: now
};
}
const bucket = buckets[userId];
const timePassed = now - bucket.lastRefill;
// Пополнить токены
const tokensToAdd = Math.floor((timePassed / 1000) * refillRate);
bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
bucket.lastRefill = now;
// Проверить наличие токенов
if (bucket.tokens >= tokensPerRequest) {
bucket.tokens -= tokensPerRequest;
return true;
}
return false;
}
4. Leaky Bucket (Протекающее ведро)
Leaky Bucket
Запросы накапливаются в очереди и обрабатываются с фиксированной скоростью. Если очередь переполнена, новые запросы отклоняются.
Преимущества:
- Ровная скорость обработки
- Хорошо для обработки очереди
- Предсказуемая производительность
Недостатки:
- Сложнее реализация с очередью
- Требует больше памяти
Сравнение стратегий
| Стратегия | Простота | Точность | Память | Burst | Когда использовать |
|---|---|---|---|---|---|
| Fixed Window | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ | Простые случаи, низкая нагрузка |
| Sliding Window | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ | Большинство случаев, средняя нагрузка |
| Token Bucket | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ | Неравномерная нагрузка, burst нужен |
| Leaky Bucket | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ❌ | Очереди запросов, ровная обработка |
Реализация на Node.js 🟢
Использование express-rate-limit
// Установка
// npm install express-rate-limit
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Базовый rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // максимум 100 запросов
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true, // Возвращает rate limit info в заголовках
legacyHeaders: false, // Отключает X-RateLimit-* заголовки
});
app.use('/api/', limiter);
// Строгий лимит для авторизации
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // максимум 5 попыток входа
skipSuccessfulRequests: true, // Не считать успешные запросы
skipFailedRequests: false,
});
app.use('/api/auth/login', authLimiter);
// Лимит на основе пользователя
const userLimiter = rateLimit({
keyGenerator: (req) => {
return req.user?.id || req.ip;
},
windowMs: 60 * 1000, // 1 минута
max: 30
});
app.use('/api/users/', userLimiter);
// Кастомный обработчик
const customLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000)
});
}
});
Использование с Redis (распределенный limiting)
// npm install rate-limiter-flexible redis
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = require('redis').createClient({
host: 'localhost',
port: 6379
});
// Rate limiter с Redis
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl', // префикс ключей в Redis
points: 100, // количество запросов
duration: 60, // за 60 секунд
});
// Middleware
async function rateLimitMiddleware(req, res, next) {
try {
const key = req.user?.id || req.ip;
await rateLimiter.consume(key);
next();
} catch (rejRes) {
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(rejRes.msBeforeNext / 1000)
});
}
}
app.use('/api/', rateLimitMiddleware);
Реализация на Python 🐍
Flask с Flask-Limiter
# pip install flask-limiter redis
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import redis
app = Flask(__name__)
# Базовый rate limiter
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379"
)
# Rate limit для конкретного endpoint
@app.route('/api/users')
@limiter.limit("10 per minute")
def get_users():
return {'users': []}
# Разные лимиты для разных методов
@app.route('/api/auth/login', methods=['POST'])
@limiter.limit("5 per 15 minutes")
def login():
return {'token': '...'}
# Лимит на основе пользователя
@limiter.limit("100 per hour", key_func=lambda: current_user.id)
@app.route('/api/profile')
def get_profile():
return {'profile': {}}
# Кастомный ключ
@limiter.limit("50 per hour", key_func=lambda: f"api_key:{request.headers.get('X-API-Key')}")
@app.route('/api/data')
def get_data():
return {'data': []}
FastAPI с slowapi
# pip install slowapi redis
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import redis
app = FastAPI()
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Redis storage
redis_client = redis.Redis(host='localhost', port=6379)
limiter.storage = redis_client
@app.get("/api/users")
@limiter.limit("10/minute")
async def get_users(request: Request):
return {"users": []}
@app.post("/api/auth/login")
@limiter.limit("5/15minutes")
async def login(request: Request):
return {"token": "..."}
Реализация на Go 🔵
// go get github.com/didip/tollbooth/v7
package main
import (
"github.com/didip/tollbooth/v7"
"github.com/didip/tollbooth/v7/limiter"
"net/http"
)
func main() {
// Создать rate limiter
lmt := tollbooth.NewLimiter(100, &limiter.ExpirableOptions{
DefaultExpirationTTL: 60000, // 60 секунд
})
// Установить лимит по IP
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
// Middleware
http.Handle("/api/", tollbooth.LimitFuncHandler(lmt, handler))
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
// С Redis
import (
"github.com/didip/tollbooth/v7/limiter"
"github.com/throttled/throttled/v2/store/memstore"
)
func createLimiterWithRedis() *limiter.Limiter {
store, _ := memstore.New(65536)
rateLimiter, _ := limiter.New(store, limiter.QRate{
Limit: 100,
Window: 60 * time.Second,
})
return rateLimiter
}
Redis для распределенного Rate Limiting 🔴
В кластерной архитектуре с несколькими серверами необходимо использовать Redis для синхронизации лимитов между серверами. Иначе каждый сервер будет вести свой счётчик, и общий лимит будет превышен.
Redis Sliding Window реализация
const redis = require('redis');
const client = redis.createClient();
async function slidingWindowRedis(key, limit, windowMs) {
const now = Date.now();
const windowStart = now - windowMs;
// Удалить старые записи
await client.zremrangebyscore(key, 0, windowStart);
// Подсчитать запросы в окне
const count = await client.zcard(key);
if (count < limit) {
// Добавить текущий запрос
await client.zadd(key, now, now);
await client.expire(key, Math.ceil(windowMs / 1000));
return { allowed: true, remaining: limit - count - 1 };
}
// Найти время следующего разрешенного запроса
const oldest = await client.zrange(key, 0, 0, 'WITHSCORES');
const retryAfter = oldest ? Math.ceil((parseInt(oldest[1]) + windowMs - now) / 1000) : 0;
return { allowed: false, retryAfter };
}
// Использование
app.use('/api/', async (req, res, next) => {
const key = `rate_limit:${req.user?.id || req.ip}`;
const result = await slidingWindowRedis(key, 100, 60000);
res.set({
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': result.remaining || 0,
'Retry-After': result.retryAfter || 0
});
if (result.allowed) {
next();
} else {
res.status(429).json({
error: 'Too many requests',
retryAfter: result.retryAfter
});
}
});
Token Bucket с Redis
-- Lua script для атомарной операции в Redis
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local tokensPerRequest = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or capacity
local lastRefill = tonumber(bucket[2]) or now
-- Пополнить токены
local timePassed = now - lastRefill
local tokensToAdd = math.floor((timePassed / 1000) * refillRate)
tokens = math.min(capacity, tokens + tokensToAdd)
-- Проверить и списать токены
if tokens >= tokensPerRequest then
tokens = tokens - tokensPerRequest
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
redis.call('EXPIRE', key, 3600)
return {1, tokens} -- allowed, remaining
else
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
redis.call('EXPIRE', key, 3600)
return {0, tokens} -- not allowed, remaining
end
// Использование Lua script
const fs = require('fs');
const luaScript = fs.readFileSync('./token_bucket.lua', 'utf8');
async function tokenBucketRedis(key, capacity, refillRate, tokensPerRequest = 1) {
const now = Date.now();
const result = await client.eval(
luaScript,
1, // количество ключей
key,
capacity,
refillRate,
tokensPerRequest,
now
);
return {
allowed: result[0] === 1,
remaining: result[1]
};
}
HTTP заголовки для Rate Limiting 📋
Важно информировать клиентов о текущих лимитах через HTTP заголовки. Это помогает клиентам правильно обрабатывать rate limiting и не делать лишние запросы.
Стандартные заголовки:
| Заголовок | Описание | Пример |
|---|---|---|
X-RateLimit-Limit |
Максимальное количество запросов | 100 |
X-RateLimit-Remaining |
Оставшееся количество запросов | 95 |
X-RateLimit-Reset |
Время сброса лимита (timestamp) | 1635436800 |
Retry-After |
Количество секунд до следующего разрешенного запроса | 60 |
// Middleware для добавления заголовков
function addRateLimitHeaders(req, res, rateLimitInfo) {
res.set({
'X-RateLimit-Limit': rateLimitInfo.limit,
'X-RateLimit-Remaining': rateLimitInfo.remaining,
'X-RateLimit-Reset': new Date(rateLimitInfo.resetTime).toISOString(),
});
// Если лимит превышен
if (!rateLimitInfo.allowed) {
res.set('Retry-After', rateLimitInfo.retryAfter);
}
}
// Использование
app.use('/api/', async (req, res, next) => {
const key = req.user?.id || req.ip;
const rateLimitInfo = await checkRateLimit(key, 100, 60000);
addRateLimitHeaders(req, res, rateLimitInfo);
if (rateLimitInfo.allowed) {
next();
} else {
res.status(429).json({
error: 'Too many requests',
message: 'Rate limit exceeded. Please try again later.',
retryAfter: rateLimitInfo.retryAfter
});
}
});
Обработка Rate Limiting на клиенте 📱
Клиентское приложение должно правильно обрабатывать ошибку 429 (Too Many Requests) и реализовать логику повторных попыток с экспоненциальной задержкой.
JavaScript/TypeScript пример:
// Функция для выполнения запроса с retry
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const response = await fetch(url, options);
// Если rate limit превышен
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
console.warn(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
// Экспоненциальная задержка
const delay = Math.min(
retryAfter * 1000,
Math.pow(2, retryCount) * 1000
);
await new Promise(resolve => setTimeout(resolve, delay));
retryCount++;
continue;
}
return response;
} catch (error) {
if (retryCount >= maxRetries) {
throw error;
}
retryCount++;
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
}
}
}
// Использование
const response = await fetchWithRetry('/api/users', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
// Обработка заголовков rate limiting
const rateLimitInfo = {
limit: parseInt(response.headers.get('X-RateLimit-Limit') || '0'),
remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '0'),
reset: new Date(response.headers.get('X-RateLimit-Reset') || Date.now())
};
console.log(`Rate limit: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}`);
console.log(`Resets at: ${rateLimitInfo.reset}`);
React Hook для rate limiting:
import { useState, useEffect } from 'react';
function useRateLimit() {
const [rateLimitInfo, setRateLimitInfo] = useState({
limit: 0,
remaining: 0,
reset: null
});
useEffect(() => {
// Обновлять информацию о rate limit при каждом запросе
const updateRateLimit = (response: Response) => {
setRateLimitInfo({
limit: parseInt(response.headers.get('X-RateLimit-Limit') || '0'),
remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '0'),
reset: new Date(response.headers.get('X-RateLimit-Reset') || Date.now())
});
};
// Перехватывать fetch
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const response = await originalFetch(...args);
updateRateLimit(response);
return response;
};
return () => {
window.fetch = originalFetch;
};
}, []);
return rateLimitInfo;
}
// Использование в компоненте
function MyComponent() {
const rateLimit = useRateLimit();
return (
Осталось запросов: {rateLimit.remaining}/{rateLimit.limit}
{rateLimit.remaining < 10 && (
Скоро достигнут лимит запросов
)}
);
}
Best Practices 🌟
✅ Рекомендации по Rate Limiting:
- Разные лимиты для разных endpoints: Авторизация строже, публичные данные мягче
- Используйте Redis для распределенного limiting: Обязательно в кластерной архитектуре
- Информируйте клиентов через заголовки: X-RateLimit-* заголовки помогают клиентам
- Возвращайте Retry-After: Клиент должен знать, когда можно повторить запрос
- Логируйте превышения лимитов: Для мониторинга и анализа атак
- Используйте whitelist для доверенных клиентов: Известные IP или API ключи
- Реализуйте graceful degradation: При превышении лимита можно вернуть ограниченные данные
- Рассмотрите tiered limits: Разные лимиты для разных типов пользователей
Примеры лимитов для разных endpoints:
| Endpoint | Лимит | Причина |
|---|---|---|
/api/auth/login |
5 запросов / 15 минут | Защита от brute-force атак |
/api/auth/register |
3 запроса / час | Защита от спама |
/api/users |
100 запросов / минута | Обычное использование |
/api/search |
30 запросов / минута | Защита от злоупотребления поиском |
/api/export |
10 запросов / час | Тяжелые операции |
Заключение
Rate Limiting — это критически важный компонент безопасности и стабильности API. Правильная реализация защищает от перегрузки, обеспечивает справедливое использование ресурсов и улучшает общий пользовательский опыт.
💡 Ключевые выводы:
- Выберите подходящую стратегию (Sliding Window рекомендуется для большинства случаев)
- Используйте Redis для распределенного limiting в кластерной архитектуре
- Информируйте клиентов через HTTP заголовки
- Реализуйте правильную обработку на клиенте с retry логикой
- Настройте разные лимиты для разных endpoints и типов пользователей
- Мониторьте и логируйте превышения лимитов для анализа
Создайте защищенный Mock API за 2 минуты
LightBox API поддерживает rate limiting из коробки. Создайте Mock API с защитой от перегрузки за 2 минуты и начните разработку безопасно.
Попробовать бесплатно →