Как реализовать ограничение частоты запросов (rate limiting) в FastAPI

Ответ

В FastAPI нет встроенного механизма для ограничения частоты запросов, но его легко интегрировать с помощью сторонних библиотек. Самым популярным решением является fastapi-limiter.

Зачем это нужно?

  • Защита от DoS-атак: Предотвращает перегрузку сервера большим количеством запросов.
  • Стабильность сервиса: Обеспечивает справедливое распределение ресурсов между пользователями.
  • Контроль доступа к API: Позволяет устанавливать разные лимиты для разных тарифных планов.

Принцип работы:

Библиотека использует хранилище (обычно Redis) для отслеживания количества запросов от каждого клиента (например, по IP-адресу) за определенный промежуток времени.

Пример с fastapi-limiter:

  1. Установка библиотек:

    pip install fastapi-limiter uvicorn redis
  2. Реализация в коде:

    from fastapi import FastAPI, Depends, Request
    from fastapi_limiter import FastAPILimiter
    from fastapi_limiter.depends import RateLimiter
    import redis.asyncio as redis
    
    app = FastAPI()
    
    @app.on_event("startup")
    async def startup():
        # Инициализация подключения к Redis
        redis_connection = redis.from_url("redis://localhost:6379", encoding="utf-8", decode_responses=True)
        await FastAPILimiter.init(redis_connection)
    
    # Применение лимита к конкретному эндпоинту
    @app.get("/limited", dependencies=[Depends(RateLimiter(times=2, seconds=5))])
    async def limited_route():
        return {"message": "Этот эндпоинт ограничен 2 запросами в 5 секунд."}
    
    # Глобальное применение лимита ко всем эндпоинтам
    @app.get("/global")
    async def global_route(request: Request):
        # Идентификатор может быть IP-адресом, токеном пользователя и т.д.
        identifier = request.client.host 
        return {"message": "Этот эндпоинт использует глобальный лимит."}

В этом примере эндпоинт /limited будет возвращать ошибку 429 Too Many Requests, если клиент сделает более 2 запросов за 5 секунд.

Ответ 18+ 🔞

Смотри, ну вот реально, блядь, как же без этого? FastAPI сам по себе, конечно, красавчик, но лимиты на запросы у него из коробки — нихуя нет. Как будто в ресторан заходишь, а охуевшие посетители могут жрать всё подряд без остановки, пока кухня не сгорит, понимаешь? Но народ не лыком шит, и для этого есть библиотека fastapi-limiter, которая всё расставит по полочкам.

А нахуя это вообще надо, спросишь?

  • Чтобы сервер не лег от какой-нибудь ебаной DoS-атаки, когда тебе начинают слать запросы, как из пулемёта.
  • Чтобы всем хватило — справедливость, блядь, чтоб один распиздяй не сожрал все ресурсы, а остальные сидели в очереди.
  • Чтобы бабки считать — можно для платных подписчиков лимиты повыше, а для халявщиков — пониже, классика.

Как это, блядь, работает?

Всё просто, как три копейки. Нужно хранилище, куда записывать, кто, сколько и когда тыкал в API. Обычно это Redis — быстрая такая штука, в память всё пишет. Туда пакуют, например, IP-адрес клиента и смотрят, не превысил ли он свой лимит за последние N секунд.

Смотри, как это в коде выглядит, ёпта:

  1. Ставим всё нужное:

    pip install fastapi-limiter uvicorn redis
  2. А вот и сам код, в рот меня чих-пых:

    from fastapi import FastAPI, Depends, Request
    from fastapi_limiter import FastAPILimiter
    from fastapi_limiter.depends import RateLimiter
    import redis.asyncio as redis
    
    app = FastAPI()
    
    @app.on_event("startup")
    async def startup():
        # Подключаемся к Redis, чтобы считать, кто сколько наворотил
        redis_connection = redis.from_url("redis://localhost:6379", encoding="utf-8", decode_responses=True)
        await FastAPILimiter.init(redis_connection)
    
    # Вешаем лимит конкретно на этот путь
    @app.get("/limited", dependencies=[Depends(RateLimiter(times=2, seconds=5))])
    async def limited_route():
        return {"message": "Этот эндпоинт ограничен 2 запросами в 5 секунд."}
    
    # А тут лимит будет глобальный, на всё подряд
    @app.get("/global")
    async def global_route(request: Request):
        # Считаем клиента, например, по IP
        identifier = request.client.host 
        return {"message": "Этот эндпоинт использует глобальный лимит."}

Вот и всё, мать его. Теперь если какой умник попробует дернуть /limited больше двух раз за пять секунд — получит в ответ ошибку 429 Too Many Requests, то есть, грубо говоря, «иди нахуй, подожди». Красота, да и только!