ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

HTTP API-шлюз на Nginx: rate limit, quota и версионирование

Разбираем, как построить лёгкий HTTP API gateway на Nginx без тяжёлых сервис-мешей: маршрутизация по версиям API, ограничение RPS и квоты по токенам, защита от абуза, JSON-обработчики ошибок и практические паттерны конфигурации для продакшн-среды.
HTTP API-шлюз на Nginx: rate limit, quota и версионирование

Идея «API gateway» чаще всего ассоциируется с тяжёлыми решениями: Envoy, Kong, Istio и прочие. Но во многих проектах вся нужная функциональность укладывается в более простую схему: Nginx на фронте как HTTP API-шлюз, пара сервисов в бэкенде и немного логики по ограничению трафика и версионированию маршрутов.

В этой статье разберём практический подход, как использовать Nginx в роли лёгкого API gateway:

  • маршрутизация запросов по версиям API (v1, v2, beta);
  • ограничение скорости (rate limit) для защиты от всплесков;
  • квоты по ключам (API key) на день/час через внешний auth-сервис или keyval/njs;
  • базовая структура конфигурации продакшн-шлюза.

Ориентируемся на сценарии «один или несколько микросервисов за Nginx», без Kubernetes и сервис-меша. Все примеры — для классического HTTP-блока Nginx. Если вы крутите API на отдельном облачном VDS, то такие конфиги удобно держать в своих include-файлах.

Базовая роль Nginx как HTTP API gateway

Типовой контур выглядит так: клиенты (мобильные приложения, SPA, внешние интеграции) бьют в единый домен API, снаружи стоит Nginx, который терминирует TLS, лимитирует и маршрутизирует запросы на разные backend-сервисы.

Клиенты → Nginx как API gateway → backend-сервисы (PHP, Node.js, Go и т.п.).

Основные задачи Nginx в этом сценарии:

  • терминация TLS и базовая безопасность HTTP;
  • маршрутизация по URI/версии/хосту к разным upstream;
  • rate limiting и квоты для защиты от злоупотреблений;
  • единый слой логирования и корреляции запросов;
  • иногда — простое кэширование идемпотентных GET.

Если у вас уже есть Nginx в роли reverse-proxy для сайтов, добавить слой API-шлюза часто можно в тот же инстанс (с отдельным server или даже хостом), что особенно логично для микросервисов на VDS. Главное — не смешивать API-конфиги и сайты: отдельные логи, отдельные upstream и явная структура include-файлов.

Структура конфигурации API-шлюза

Для начала зафиксируем общую структуру nginx.conf (или включаемых файлов), чтобы меньше путаться, когда будем добавлять rate limit, квоты и версионирование.

worker_processes auto;

http {
    include mime.types;
    default_type application/octet-stream;

    # Логи и базовые настройки опускаем ради краткости

    # Зоны и лимиты для rate limit
    limit_req_zone $binary_remote_addr zone=rl_ip_1m:10m rate=10r/s;

    # Ключи kv для динамических квот, если используете nginx-plus/njs+keyval
    # keyval_zone ...;

    upstream api_v1_upstream {
        server 127.0.0.1:9001;
    }

    upstream api_v2_upstream {
        server 127.0.0.1:9002;
    }

    server {
        listen 80;
        server_name api.example.com;

        # Здесь будем строить маршрутизацию и лимиты
    }
}

Дальше углубимся в каждый аспект: версионирование, rate limit и квоты. Структура конфига важна, чтобы изменения в одной зоне не ломали остальные сервисы при деплое.

Экран администратора с конфигурацией Nginx для версионирования HTTP API

Версионирование API в Nginx

Версионирование API — это не только вопрос URL-дизайна, но и вопрос маршрутизации. Нам нужно уметь прозрачно развести трафик:

  • между /v1/ и /v2/;
  • между стабильной и beta-версией;
  • между клиентами с разными заголовками (feature flags, mobile/web).

Версия в URI: самый прямолинейный вариант

Классический способ: версия — часть пути /v1/.... Для Nginx это очень удобно, мы просто создаём отдельные location и upstream:

server {
    listen 80;
    server_name api.example.com;

    # v1
    location ^~ /v1/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_pass http://api_v1_upstream;
    }

    # v2
    location ^~ /v2/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_pass http://api_v2_upstream;
    }
}

Плюсы: простота, явность, удобно логировать и мониторить по префиксу URI. Минус — иногда неудобно менять клиентов, но это уже вопрос API-дизайна и версионирования контрактов.

Версия в заголовке или параметре

Если вы используете версию в заголовке, вроде X-API-Version: 2, маршрутизацию удобно строить через map:

http {
    map $http_x_api_version $api_upstream_name {
        default api_v1_upstream;
        2 api_v2_upstream;
        2-beta api_v2_beta_upstream;
    }

    upstream api_v1_upstream {
        server 127.0.0.1:9001;
    }

    upstream api_v2_upstream {
        server 127.0.0.1:9002;
    }

    upstream api_v2_beta_upstream {
        server 127.0.0.1:9003;
    }

    server {
        listen 80;
        server_name api.example.com;

        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_pass http://$api_upstream_name;
        }
    }
}

Такая схема позволяет без изменения URI мягко переключать клиентов на новую версию, а также делать beta-каналы по заголовкам или query-параметрам (через map по $arg_version).

Советы по versioning в Nginx

  • Всегда добавляйте в логи версию API (из URI или заголовка) — это сильно помогает при разборе проблем.
  • Для миграций между версиями можно временно проксировать часть методов в старый backend, а часть — в новый, используя разные location и map.
  • Не смешивайте логическую версию (в протоколе) и технический путь — делайте явную прослойку с map, тогда меньше боли при рефакторинге.

Rate limit в Nginx как защита от всплесков

Ограничение скорости (rate limit) — базовый инструмент защиты API от злоупотреблений и ошибок интеграторов. В Nginx есть готовые директивы: limit_req_zone и limit_req. Если их аккуратно комбинировать с несколькими зонами, можно довольно тонко настроить поведение шлюза.

Базовый rate limit по IP

Простейший пример — ограничиваем каждое IP до 10 запросов в секунду со всплеском до 20 (burst):

http {
    # Создаём зону для хранения счётчиков
    limit_req_zone $binary_remote_addr zone=rl_ip_1m:10m rate=10r/s;

    server {
        listen 80;
        server_name api.example.com;

        location / {
            # Применяем лимит к этому location
            limit_req zone=rl_ip_1m burst=20 nodelay;

            proxy_pass http://api_v1_upstream;
        }
    }
}

Семантика:

  • rate=10r/s — «плавная» скорость;
  • burst=20 — допуск кратковременного всплеска поверх этой скорости;
  • nodelay — отклонить лишние запросы сразу, не ставить в очередь.

По умолчанию при превышении лимита возвращается 503. Это можно поменять через limit_req_status, но для API часто логичнее использовать 429 (Too Many Requests):

http {
    limit_req_status 429;
}

Rate limit по API-ключу, а не по IP

Для публичного API лимит по IP часто бесполезен (NAT, мобильные сети). Логичнее ограничивать по API-ключу или токену.

Предположим, клиент передаёт ключ в заголовке X-API-Key. Создадим переменную, которая берёт либо ключ, либо IP (fallback):

http {
    map $http_x_api_key $rl_key {
        default $http_x_api_key;
        "" $binary_remote_addr;
    }

    limit_req_zone $rl_key zone=rl_api_key:20m rate=5r/s;

    server {
        listen 80;
        server_name api.example.com;

        location / {
            limit_req zone=rl_api_key burst=10 nodelay;
            proxy_pass http://api_v1_upstream;
        }
    }
}

Теперь каждый уникальный API-ключ имеет собственный лимит 5 запросов в секунду, а клиенты без ключа ограничиваются по IP. Обратите внимание на размер зоны и количество ожидаемых ключей — при высокой нагрузке память можно проесть неожиданно быстро.

Глобальные и пер-методные лимиты

Иногда нужны разные лимиты для разных endpoint: например, GET /status можно звать хоть каждую секунду, а POST /payments — максимум раз в несколько секунд.

В этом случае создавайте несколько зон:

http {
    # Глобальный лимит
    limit_req_zone $rl_key zone=rl_global:20m rate=20r/s;

    # Более жёсткий лимит для write-операций
    limit_req_zone $rl_key zone=rl_write:10m rate=5r/s;

    server {
        listen 80;
        server_name api.example.com;

        # Применяем глобальный лимит ко всему API
        location / {
            limit_req zone=rl_global burst=40 nodelay;
            proxy_pass http://api_v1_upstream;
        }

        # Но для write-эндпоинтов добавляем второй лимит
        location ~ ^/v1/(payments|orders) {
            limit_req zone=rl_global burst=40 nodelay;
            limit_req zone=rl_write burst=10 nodelay;
            proxy_pass http://api_v1_upstream;
        }
    }
}

В этом примере write-эндпоинты получат ограничение и по глобальной квоте, и по более жёсткой write-квоте. Подобный паттерн удобно расширять новыми зонами для «дорогих» методов.

Схема взаимодействия клиентов с Nginx, реализующим rate limit и квоты перед backend-сервисами

Квоты (quota): дневные и часовые лимиты на API

Rate limit ограничивает мгновенную скорость, но не покрывает сценарии «не более 1000 запросов в день на ключ» или «не более 100 запросов в час на тяжёлый endpoint». Такие ограничения удобнее моделировать как квоты.

Чисто на стандартном Nginx полноценные счётчики квот по времени не реализуешь — понадобится внешний стор (Redis, БД) или платные/расширенные возможности (keyval, njs). Но можно собрать рабочий вариант в двух подходах:

  • использовать Redis (или иной backend) и проверять квоту через отдельный auth-сервис;
  • использовать Nginx + njs + keyval (или nginx-plus) для хранения счётчиков.

Рассмотрим оба на концептуальном уровне, чтобы понимать, куда вырастать из простого rate limit по IP.

Подход 1: квоты через внешний auth-сервис

Идея: перед тем как пропустить запрос в основной backend, отправляем internal-запрос в сервис авторизации/квот, который уже сам обновляет счётчики в Redis/БД и решает, можно или нельзя. В Nginx это делается через auth_request.

upstream api_backend {
    server 127.0.0.1:9001;
}

upstream auth_quota_backend {
    server 127.0.0.1:9100;
}

server {
    listen 80;
    server_name api.example.com;

    # Все запросы сначала идут на проверку квоты
    location / {
        auth_request /_auth_quota;
        auth_request_set $quota_status $upstream_status;

        proxy_pass http://api_backend;
    }

    # Внутренний location для auth_request
    location = /_auth_quota {
        internal;
        proxy_set_header X-API-Key $http_x_api_key;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://auth_quota_backend;
    }
}

Дальше уже логика на стороне auth-сервиса:

  • он проверяет токен/ключ;
  • смотрит в Redis счётчик за день/час;
  • при необходимости инкрементирует и возвращает 200 (разрешено) или 429/403 (квота исчерпана или запрещено).

Плюсы подхода:

  • гибкость (любые квоты, какие захотите);
  • единая точка правил для всех API (можно использовать и в других сервисах, не только HTTP);
  • масштабирование отдельно от Nginx.

Минусы:

  • дополнительный hop и задержка;
  • нужно писать и поддерживать отдельный сервис.

Подход 2: квоты на Nginx через keyval и njs

Если вы готовы использовать модуль njs и keyval (либо коммерческую версию Nginx), можно реализовать простые квоты прямо в Nginx, храня счётчики в памяти (или shm). Концептуально это выглядит так:

  1. заводим key-value хранилище для счётчиков по ключу (API key + день/час);
  2. в njs-скрипте инкрементируем значение и решаем, превышена квота или нет;
  3. возвращаем 200 или 429 в зависимости от результата.

Общий минус: данные в памяти не переживут рестарт/перезапуск Nginx, поэтому такой подход подходит скорее для мягких квот, а не строгой биллинговой логики. Для биллинг-критичных ограничений лучше держать отдельный сервис и устойчивое хранилище.

Связка rate limit + quota: зачем нужно оба

Rate limit и квоты решают разные задачи, и в продакшн API обычно нужны оба:

  • rate limit — защита от мгновенных всплесков, DoS, ошибок в циклах;
  • quota — защита от «медленного» выжигания ресурсов (миллионы запросов за день).

Типичная схема:

  • на Nginx вешаем относительно мягкий, но обязательный rate limit (например, до 50 rps на ключ);
  • на уровне auth/quota-сервиса (или в приложении) считаем дневные и часовые лимиты;
  • при превышении квоты возвращаем 429 с описанием в JSON.

Таким образом Nginx «заглушает» аномальные пики, а бизнес-логика квот остаётся в вашем контроле и может развиваться независимо. Если вы ещё не внедрили квоты, начать стоит хотя бы с базового rate limit, он закрывает большую часть инцидентов.

Практические паттерны маршрутизации API

Помимо версионирования, полезно обратить внимание на ещё пару паттернов, типичных для API gateway на Nginx. Они несложные, но сильно облегчают сопровождение и расследование инцидентов.

Разделение публичного и внутреннего API

Желательно разделять публичные и internal endpoint хотя бы по префиксу или хосту. Например:

  • api.example.com — публичный API;
  • internal-api.example.com или /internal/... — служебные и админские методы.

В конфиге это легко выразить разными server или location, где на публичном включены rate limit и аутентификация, а на внутреннем — только из доверенных сетей:

server {
    listen 80;
    server_name api.example.com;

    # Публичный API с лимитами
    location /v1/ {
        allow all;
        limit_req zone=rl_api_key burst=10 nodelay;
        proxy_pass http://api_v1_upstream;
    }

    # Внутреннее API для сервисов
    location /internal/ {
        allow 10.0.0.0/8;
        deny all;
        proxy_pass http://api_v1_upstream;
    }
}

Такая изоляция сильно снижает риск случайной публичной экспозиции внутренних методов. При миграции внутренних сервисов без даунтайма пригодятся практики из материала о переезде проектов без простоя.

Добавление корреляционных ID

Для API-шлюза почти обязательно добавлять корреляционный идентификатор запроса (например, X-Request-ID), чтобы связывать логи Nginx и бэкенда. Если клиент не прислал свой ID, Nginx может сгенерировать его, например, с помощью модуля njs.

Простейший вариант без njs — доверять ID от клиента и форсировать его логирование:

log_format api_log '$remote_addr - $remote_user [$time_local] '
                  '"$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" '
                  'req_id="$http_x_request_id" api_key="$http_x_api_key" api_ver="$http_x_api_version"';

access_log /var/log/nginx/api.access.log api_log;

Если вы генерите ID на стороне бэкенда, убедитесь, что он возвращается в заголовке ответа и попадает в логи Nginx (через $upstream_http_x_request_id), иначе корреляция логов развалится.

Ошибки и ответы API через Nginx

API-шлюз часто отвечает клиенту не только успехами, но и ошибками: rate limit exceeded, quota exceeded, bad auth, maintenance. Это можно элегантно реализовать в Nginx через error_page и return, чтобы всегда отдавать JSON вместо HTML-заглушек.

Читабельный 429 для rate limit

По умолчанию при превышении лимита Nginx вернёт голый HTML. Для JSON-API это неудобно, поэтому лучше сделать собственный ответ:

server {
    listen 80;
    server_name api.example.com;

    # Если лимит сработал — отправляем на спец-страницу
    error_page 429 = @rate_limited;

    location / {
        limit_req zone=rl_api_key burst=10 nodelay;
        proxy_pass http://api_v1_upstream;
    }

    location @rate_limited {
        add_header Content-Type application/json;
        return 429 '{"error":"rate_limited","message":"Too many requests"}';
    }
}

Аналогично можно описать ответы для временного отключения сервиса (503) или запрета (403), сохранив единый JSON-формат. Единственное — следите за экранированием кавычек в JSON, иначе перезагрузка конфига не взлетит.

Версионирование и миграции без даунтайма

При выпуске новой версии API задача шлюза — позволить вам мягко мигрировать трафик:

  • запустить v2 параллельно с v1;
  • часть клиентов (бета-тестеры) направить на v2 через заголовки или отдельный хост;
  • поддерживать v1 до окончания переходного периода;
  • затем аккуратно срезать маршруты v1.

В Nginx это делается через комбинацию map и upstream, о которой мы говорили выше. Дополнительно можно сделать «partial rollout» по заголовку:

map $http_x_beta_user $api_upstream_name {
    default api_v1_upstream;
    1 api_v2_upstream;
}

server {
    listen 80;
    server_name api.example.com;

    location /v1/ {
        proxy_pass http://$api_upstream_name;
    }
}

Тем, у кого X-Beta-User: 1, вы отдаёте те же URI, но обслуживаемые новым backend. Это удобно для A/B-тестов и поэтапной миграции логики без жёсткого переключения всего трафика.

Рекомендации по продакшн-настройке API-шлюза на Nginx

Соберём основные выводы в виде практических советов для продакшн-конфига:

  • явно отделяйте API-конфиг от конфигов обычных сайтов: отдельные server, отдельные логи, отдельные upstream;
  • включайте базовый rate limit: даже мягкий (50–100 rps на ключ/IP) уже сильно уменьшит риск случайных DoS;
  • квоты не пытайтесь целиком реализовать «на чистом Nginx» — лучше держать отдельный auth/quota-сервис с Redis или БД, а Nginx использовать как тонкий слой маршрутизации и rate limit;
  • логируйте ключевые признаки: API-версию, API-ключ/клиента, request id, код ответа, latency;
  • тестируйте миграции версий через staging и нагрузочные тесты, прежде чем переключать реальный трафик;
  • следите за размерами зон (limit_req_zone) и количеством уникальных ключей — при больших объёмах трафика это ощутимая память;
  • не забывайте про TLS: для боевого API нужны нормальные цепочки сертификатов и грамотный TLS-конфиг, особенно если вы используете HSTS и HTTP/2; детали можно подсмотреть в статье о миграции домена, HSTS и SSL. Для публичного API сразу планируйте установку надёжных SSL-сертификаты, чтобы не упираться в предупреждения браузеров и ограничения клиентов.

Лёгкий HTTP API gateway на Nginx хорошо закрывает потребности малого и среднего проекта, экономя ресурсы и время на внедрение сложных решений. При разумной архитектуре и аккуратной конфигурации он без проблем выдерживает серьёзную продакшн-нагрузку и становится удобной точкой расширения, если позже вы решите добавить более продвинутые фичи.

Поделиться статьей

Вам будет интересно

Bash one-liners с jq, curl и xargs: практические рецепты для админов OpenAI Статья написана AI (GPT 5)

Bash one-liners с jq, curl и xargs: практические рецепты для админов

Разбираем практические Bash one-liners с jq, curl и xargs: как быстро выдёргивать данные из JSON, пачками дергать HTTP API, провер ...
Nginx + бесплатный cookie-free CDN как origin: пошаговая схема и тонкая настройка OpenAI Статья написана AI (GPT 5)

Nginx + бесплатный cookie-free CDN как origin: пошаговая схема и тонкая настройка

Разбираем, как подключить сайт на Nginx к бесплатному CDN и использовать сервер как origin без сюрпризов. Пошагово настраиваем coo ...
Автобэкапы в Object Storage: rclone + cron для файлов, MySQL и PostgreSQL OpenAI Статья написана AI (GPT 5)

Автобэкапы в Object Storage: rclone + cron для файлов, MySQL и PostgreSQL

Разберем, как на VDS настроить автоматические резервные копии в S3‑совместимый Object Storage с помощью rclone и cron. Покажу базо ...