API Rate Limiting: как защитить API от перегрузки

← Вернуться к статьям

Введение

Rate Limiting (ограничение частоты запросов) — это критически важный механизм защиты API от перегрузки, злоупотребления и DDoS атак. Без rate limiting ваш API может быть перегружен большим количеством запросов от одного клиента, что приведёт к недоступности сервиса для других пользователей.

В этом руководстве мы рассмотрим различные стратегии rate limiting, примеры реализации на популярных языках программирования, использование Redis для распределенного limiting, и best practices для защиты вашего API.

✅ Что вы узнаете:

💡 Зачем нужен rate limiting:

📋 Содержание

Что такое Rate Limiting? 🎯

Rate Limiting — это механизм контроля количества запросов, которые клиент может сделать к API за определенный период времени. Когда лимит превышен, API возвращает HTTP статус 429 (Too Many Requests).

Типы лимитов:

Примеры лимитов:

Стратегии Rate Limiting 📊

1. Fixed Window (Фиксированное окно)

Fixed Window

Самый простой подход. Время делится на фиксированные интервалы (например, минуты), и каждое окно имеет свой лимит запросов.

Преимущества:

Недостатки:

// 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

Окно времени скользит вместе с запросами. Каждый запрос имеет время истечения, и окно постоянно обновляется.

Преимущества:

Недостатки:

// 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

Токены добавляются в "ведро" с фиксированной скоростью. Каждый запрос потребляет токен. Если токены закончились, запрос блокируется.

Преимущества:

// 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:

Endpoint Лимит Причина
/api/auth/login 5 запросов / 15 минут Защита от brute-force атак
/api/auth/register 3 запроса / час Защита от спама
/api/users 100 запросов / минута Обычное использование
/api/search 30 запросов / минута Защита от злоупотребления поиском
/api/export 10 запросов / час Тяжелые операции

Заключение

Rate Limiting — это критически важный компонент безопасности и стабильности API. Правильная реализация защищает от перегрузки, обеспечивает справедливое использование ресурсов и улучшает общий пользовательский опыт.

💡 Ключевые выводы:

Создайте защищенный Mock API за 2 минуты

LightBox API поддерживает rate limiting из коробки. Создайте Mock API с защитой от перегрузки за 2 минуты и начните разработку безопасно.

Попробовать бесплатно →
← Вернуться к статьям