
Бенчмарк, который померил не то
Перепроверил статью про HAProxy, Envoy, nginx, Traefik и Caddy. Весь их рейтинг переворачивается от одной строчки конфига, а nginx по дороге потерял половину скорости на смене дефолта.
С чего началось
Мы держим Traefik. Живём с ним давно, он стабилен, и никаких причин его трогать не было.
Потом мне попалась статья на Хабре от апреля 2025-го. Пять реверс-прокси на 50 000 запросов в секунду. Итог такой:
| HAProxy | Envoy | nginx | Traefik | Caddy | |
|---|---|---|---|---|---|
| RPS | 49 728 | 49 749 | 9 158 | 4 659 | 3 354 |
| ошибок | 0% | 0% | 10.2% | 32.8% | 35.9% |
| p95 | 1.16 ms | 3.34 ms | 3.82 s | 8.46 s | 17.03 s |
| CPU | 9 ядер | 16 ядер | 30 ядер | 30 ядер | 30 ядер |
Traefik “не справился с нагрузкой”. Тот самый, который у нас в проде не жалуется.
Дальше можно было пойти двумя путями. Поверить и начать мигрировать. Или проверить.
Первое, что царапнуло
Открываем конфиги из статьи. Вот nginx:
location / {
proxy_pass http://10.0.0.4:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
...
}Вот HAProxy целиком:
defaults
mode http
frontend main
bind *:443 ssl crt /usr/local/etc/haproxy/certs/haproxy.pem
default_backend test_backend
backend test_backend
server test_backend_server 10.0.0.8:8080 checkНи у кого не настроен пул соединений до бэкенда. Ни у кого. И это выглядит честно - все в равных условиях, все на дефолтах.
Только дефолты у них разные:
| прокси | пул до бэкенда по умолчанию |
|---|---|
| HAProxy | переиспользование включено |
| Envoy | пул без явного лимита |
| Traefik | maxIdleConnsPerHost = 200 |
| Caddy | keepalive_idle_conns_per_host = 32 |
| nginx | кэша нет, если не завёл upstream-блок |
Сравните этот столбик с рейтингом из статьи. Это один и тот же порядок.
Совпадение? Надо мерить.
Стенд
Собрал стенд, всё в Docker на одной машине. Бэкенд, прокси и генератор нагрузки разведены по непересекающимся наборам ядер через --cpuset-cpus - иначе меряется не прокси, а драка трёх процессов за планировщик.
| роль | ядра |
|---|---|
бэкенд (Go + fasthttp, отдаёт hello world) | 0-2 |
| прокси | 3-6 |
| генератор нагрузки | 7-11 |
Железо и окружение:
| машина | MacBook Pro, Apple M4 Max, 16 ядер (12 performance + 4 efficiency), 48 GB |
| ОС | macOS 26.5.2 (25F84), arm64 |
| Docker | Docker Desktop 29.7.2, Apple Virtualization Framework + VirtioFS |
| VM | 12 vCPU, 12 GiB RAM, swap 4 GiB, ядро 7.0.12-linuxkit, overlay2, aarch64 |
| сборка Go-бинарей | golang:1.25-alpine, go1.25.14 |
| обработка результатов | Python 3.14.4 |
Образы - официальные, по тегу latest на 23 августа 2026:
| образ | что внутри |
|---|---|
haproxy:latest | HAProxy 3.4.3, OpenSSL 3.5.6 |
envoyproxy/envoy:distroless-v1.36-latest | Envoy 1.36.9, BoringSSL |
nginx:latest | nginx 1.31.4, OpenSSL |
traefik:latest | Traefik 3.7.11, Go 1.26.6 |
caddy:latest | Caddy 2.11.4 |
grafana/k6:latest | k6 2.2.0 |
alpine:3 | wrk 4.x, curl, openssl |
Для сравнения с апрелем 2025 брал haproxy:3.1.6, nginx:1.27.4, envoyproxy/envoy:v1.34.0, traefik:v3.3.5, caddy:2.9.1. Для бисекта nginx - все теги от 1.27.4 до 1.31.4. Полный список с sha256-дайджестами лежит в репозитории.
Честная оговорка: 12 vCPU виртуалки ложатся на 16 физических ядер M4 Max, где ядра разные - performance и efficiency. Какой vCPU куда попадёт, я не контролирую. Поэтому абсолютные RPS тут - цифры этой конкретной машины, а не паспортные характеристики прокси. Сравнивать между собой их можно (все прогоны в одинаковых условиях, повторы сходятся в пределах 2-5%), переносить на свой кластер - нет.
Главное решение - не мерить RPS как основную метрику. RPS зависит от того, сколько ядер вы дали и не упёрся ли генератор. Мерю микросекунды CPU прокси на один запрос. Это свойство прокси и его конфига, а не стенда. CPU беру кумулятивно из Docker API, а не усреднённым процентом из docker stats.
Весь стенд лежит на github.com/haiodo/reverse-proxy-bench. Три команды - и вы повторяете всё сами.
Замер первый: те же конфиги
Клиент с keep-alive, ECDSA P-256, 200 одновременных соединений.
| прокси | RPS | p99 | µs CPU/запрос |
|---|---|---|---|
| HAProxy | 223 220 | 1.51 ms | 17.8 |
| Envoy | 121 992 | 8.73 ms | 32.7 |
| Traefik | 94 102 | 6.49 ms | 41.5 |
| Caddy | 53 205 | 12.75 ms | 71.2 |
| nginx | 32 419 | 354 ms | 121.8 |
Первое, что бросается в глаза: HAProxy на четырёх ядрах ноутбука выдал 223 тысячи. В статье он на 32 vCPU показал 50 тысяч и загрузил 9 ядер. Разница в эффективности - в десять раз. Значит, в той статье HAProxy не проксировал, а занимался чем-то другим.
Второе: Traefik на втором-третьем месте с нулём ошибок. Никакого “не справился”.
Порядок при этом ровно тот же, что в статье. Причину я уже назвал.
Замер второй: одна строчка
Добавляю каждому пул до бэкенда. Больше ничего не трогаю.
| прокси | было | стало | µs CPU/запрос |
|---|---|---|---|
| nginx | 32 419 | 205 308 | 121.8 → 17.9 |
| Caddy | 53 205 | 64 897 | 71.2 → 56.9 |
| Traefik | 94 102 | 98 843 | 41.5 → 39.6 |
| HAProxy | 223 220 | 228 181 | уже был включён |
| Envoy | 121 992 | 119 418 | уже был включён |
nginx - в шесть раз. Хвост латентности p99 с 354 ms до 6.5 ms.
А если довести настройку до конца, nginx выдаёт 261 515 RPS и становится первым. Тот самый nginx, который в статье последний с 10% ошибок.
Замер третий: ручка в обратную сторону
Это важнее предыдущего. Беру тех, у кого пул включён по умолчанию, и выключаю его руками.
| прокси | ручка | было | стало | µs CPU/запрос |
|---|---|---|---|---|
| Envoy | max_requests_per_connection: 1 | 121 992 | 31 970 | 32.7 → 124.7 |
| Traefik | maxIdleConnsPerHost: -1 | 94 102 | 27 497 | 41.5 → 131.7 |
| Caddy | keepalive off | 53 205 | 34 463 | 71.2 → 109.4 |
| HAProxy | option http-server-close | 223 220 | 82 383 | 17.8 → 48.1 |
Все сваливаются в одну яму: 27-34 тысячи и 110-132 микросекунды. Ровно туда, где сидел nginx с дефолтом.
Вот и ответ. Разница между продуктами на порядок меньше, чем разница между “пул есть” и “пула нет” внутри любого из них. Рейтинг из статьи - это не рейтинг прокси. Это рейтинг их дефолтных настроек пула, отсортированный по размеру.
Кстати, отдельная засада: у HAProxy http-reuse never не выключает keep-alive. Он запрещает делить соединение между клиентами, но внутри сессии соединение живёт. Замер с never дал 233 тысячи, то есть ничего не изменилось. Реально выключает только option http-server-close.
Куда делся Traefik
Осталось объяснить, почему в статье Traefik не просто медленный, а именно разваливается: 33% ошибок, p95 восемь секунд.
Смотрим на конфиг Traefik из статьи. Он получает сертификат сам, через ACME:
--certificatesresolvers.myresolver.acme.httpchallenge=true
--certificatesresolvers.myresolver.acme.email=example@yandex.comА остальные четверо берут готовый файл от certbot.
$ docker run --rm traefik:latest traefik --help | grep -A1 keytype
--certificatesresolvers.<name>.acme.keytype (Default: "RSA4096")RSA-4096. Caddy по умолчанию берёт ec256. certbot с версии 2.0 - тоже ECDSA P-256. Traefik остаётся единственным, кто подписывает каждый TLS-хендшейк самым дорогим ключом из существующих.
Померил стоимость полного хендшейка, микросекунды CPU:
| прокси | EC256 | RSA-2048 | RSA-4096 |
|---|---|---|---|
| Envoy (BoringSSL) | 164 | 499 | 2 367 |
| nginx (OpenSSL) | 266 | 614 | 2 544 |
| HAProxy (OpenSSL) | 283 | 615 | 2 496 |
| Traefik (Go) | 282 | 1 036 | 4 827 |
| Caddy (Go) | 355 | 1 112 | 4 914 |
Дефолт Traefik стоит 4 827 µs. Дефолт Caddy - 355 µs. Разница в 13.6 раза, и она не про Traefik, а про выбор ключа.
Ну и до кучи: Go-шная криптография на RSA примерно вдвое дороже OpenSSL, а на эллиптике идёт вровень. То есть выбор RSA4096 дефолтом бьёт по Go-прокси дважды.
Проверяем в лоб. Тот же бинарь, тот же конфиг, открытая модель нагрузки на 15 000 запросов в секунду, меняется только файл сертификата:
| Traefik, ключ | RPS | p95 | VU у генератора |
|---|---|---|---|
| EC256 | 14 987 | 0.26 ms | 508 |
| RSA-2048 | 14 914 | 0.29 ms | 1 197 |
| RSA-4096 | 2 382 | 13.6 s | 20 000 (потолок) |
Вот он, обвал из статьи. Воспроизвёлся один в один. Traefik тут ни при чём.
Если вам интересно, RSA-4096 валит всех, просто в порядке стоимости их хендшейка:
| прокси на RSA-4096 | RPS | p95 | VU у генератора | ядер прокси (из 4) |
|---|---|---|---|---|
| HAProxy | 14 908 | 0.20 ms | 984 | 0.70 |
| Envoy | 13 992 | 6.5 ms | 6 534 | 1.36 |
| nginx | 10 659 | 3.5 s | 20 000 | 2.36 |
| Caddy | 3 819 | 6.5 s | 20 000 | 3.75 |
| Traefik | 2 382 | 13.6 s | 20 000 | 3.70 |
Как именно оно разваливается
Посмотрите на столбик VU в двух последних таблицах. Там вся механика.
Генератор работает по открытой модели: “выдавай 15 000 запросов в секунду, чего бы это ни стоило”. Пока прокси отвечает быстро, хватает пятисот виртуальных пользователей, соединения переиспользуются, хендшейков почти нет.
Стоит латентности подрасти - генератор поднимает новых VU, чтобы держать заданный темп. Каждый новый VU открывает новое соединение. Каждое новое соединение - это TLS-хендшейк. На дешёвом ключе прокси это переваривает. На RSA-4096 он захлёбывается, латентность растёт ещё, генератор поднимает ещё VU, и так до упора в maxVUs.
Это положительная обратная связь. Она меряет не потолок прокси, а скорость схлопывания стенда.
Отдельно откалибровал сам генератор - HAProxy, дешёвый ключ, растущий заказанный темп:
| заказано | достигнуто | ядер прокси (из 4) | p95 | потеряно итераций |
|---|---|---|---|---|
| 10 000 | 9 994 | 0.45 | 0.21 ms | 110 |
| 20 000 | 19 956 | 0.82 | 0.20 ms | 856 |
| 40 000 | 39 875 | 1.47 | 2.5 ms | 2 468 |
| 80 000 | 69 693 | 2.02 | 24 ms | 205 481 |
На 80 тысячах тест “проваливается” - а прокси занят на половину. Тот же HAProxy на закрытой модели спокойно отдаёт 223 тысячи.
В статье, напомню, у “победителя” HAProxy тоже потеряно 16 192 итерации. Генератор уже упирался в себя на первом же тесте.
nginx, который потерял половину
Тут я собирался закончить, но решил прогнать ещё и те же версии, что были в статье. Апрель 2025-го против августа 2026-го, один и тот же стенд, один и тот же конфиг.
| прокси | апрель 2025 | август 2026 | дельта |
|---|---|---|---|
| HAProxy | 3.1.6 - 237 457 | 3.4.3 - 223 220 | -6% |
| Envoy | 1.34.0 - 117 196 | 1.36.9 - 121 992 | +4% |
| Traefik | 3.3.5 - 99 689 | 3.7.11 - 94 102 | -6% |
| Caddy | 2.9.1 - 54 504 | 2.11.4 - 53 205 | -2% |
| nginx | 1.27.4 - 76 530 | 1.31.4 - 32 419 | -58% |
Четверо стоят на месте в пределах разброса. nginx на том же конфиге просел в 2.4 раза.
Прогнал все версии подряд:
| версия | RPS | µs/запрос | p99 |
|---|---|---|---|
| 1.27.4 - 1.29.6 | 73 686 - 76 250 | 51 - 53 | 12-18 ms |
| 1.29.7 | 32 390 | 121.5 | 425 ms |
| 1.29.8 - 1.31.4 | 32 333 - 32 417 | ~121 | ~360 ms |
Обвал ровно на 1.29.7, 24 марта 2026. Открываем CHANGES:
*) Change: now the "keepalive" directive in the "upstream" block is enabled by default.
*) Change: now ngx_http_proxy_module supports keepalive by default; the default value
for "proxy_http_version" is "1.1"; the "Connection" proxy header is not sent by
default anymore.nginx включил keep-alive до бэкенда по умолчанию. Изменение, задуманное как ускорение, дало замедление вдвое.
Разбираемся. Изолирующие замеры на 1.31.4, базовая линия - конфиг из статьи, 32 342 RPS:
| конфиг | RPS | µs/запрос |
|---|---|---|
обернуть в upstream-блок, ни строчки больше | 191 798 | 19.0 |
upstream + keepalive 128 | 209 107 | 17.7 |
upstream + keepalive 32 | 197 739 | 18.7 |
upstream + keepalive 32 local | 190 577 | 19.2 |
upstream + keepalive 2 | 97 339 | 40.3 |
конфиг из статьи + вернуть Connection: close | 74 416 | 52.6 |
Дефолт - keepalive 32 local. Но явные 32 дают 198 тысяч, а дефолтные 32 - 32 тысячи. Параметр local тоже ни при чём (190 577 против 190 266 на повторе).
Решает наличие upstream-блока. Дефолтный кэш соединений работает только для именованного upstream. При proxy_pass http://host:port с литеральным адресом кэша нет - но Connection: close бэкенду nginx с 1.29.7 больше не отправляет. Соединение открывается по HTTP/1.1, используется один раз и закрывается самим nginx.
Худшее из двух миров. Раньше закрывал бэкенд, теперь закрывает nginx - и платит за это.
Вывод простой: заводите upstream-блок, даже если бэкенд один. Одно это, без единой дополнительной директивы, даёт шестикратный прирост.
Вебсокеты
Раз уж стенд стоял, добавил вебсокеты - у нас на них живёт весь продукт.
Эхо-сообщения, 200 соединений:
| прокси | сообщений/с | p99 | RSS |
|---|---|---|---|
nginx (с Upgrade) | 338 736 | 4.78 ms | 103 MB |
| HAProxy | 299 529 | 2.38 ms | 97 MB |
| Caddy | 297 828 | 2.54 ms | 71 MB |
| Traefik | 295 190 | 2.50 ms | 96 MB |
| Envoy | 284 102 | 7.01 ms | 44 MB |
| nginx (конфиг из статьи) | 0 | - | 200 отказов из 200 |
Все в пределах 15% друг от друга - релеинг вебсокетов дёшев и одинаков. Кроме nginx с конфигом из статьи: там нет проброса Upgrade, и вебсокеты не проходят вообще. У остальных четверых работают из коробки.
Держим 5000 соединений:
| прокси | установлено | p99 | RSS |
|---|---|---|---|
| HAProxy | 5000/5000 | 2-3 ms | 371 MB |
| Caddy | 5000/5000 | 4.5 ms | 698 MB |
| Traefik | 5000/5000 | 5.0 ms | 632 MB |
| nginx | 5000/5000 | 97-155 ms | 375 MB |
| Envoy | 1024/5000 | - | 3976 отказов |
Envoy упёрся ровно в 1024 - это дефолтный порог circuit breaker max_connections у кластера. Каждый вебсокет держит соединение к бэкенду. Поднимаем порог - 5000 из 5000.
nginx на длинных простаивающих соединениях стабильно хуже всех по хвостам: 97 и 155 ms в двух прогонах против 2-3 ms у HAProxy.
Что я забрал себе
Мерить надо CPU на запрос, а не RPS. RPS говорит про ваш стенд. Микросекунды CPU на запрос говорят про прокси. Первое нельзя переносить между машинами, второе можно.
Открытая модель нагрузки не меряет потолок. Она меряет, как система разваливается. Это отдельный полезный тест, но потолок ищут закрытой моделью с фиксированной конкурентностью. Иначе вы публикуете скорость схлопывания собственного генератора.
“Все на дефолтах” - это не равные условия. Это пять разных конфигураций, про которые вы не знаете, чем они отличаются. Если сравниваете - сначала выпишите дефолты в таблицу и посмотрите, не совпадает ли она с вашим будущим рейтингом.
Сертификат всем один и тот же файл. Не три разных ACME-клиента с тремя разными дефолтами типа ключа. Разница между EC256 и RSA-4096 - тринадцатикратная, она перекрывает любую разницу между продуктами.
Дефолты меняются, и не всегда в вашу пользу. nginx поменял поведение keep-alive, целясь в ускорение, и для конфигов без upstream-блока получилось замедление вдвое. Ваш конфиг работает не так, как когда вы его писали.
А Traefik мы оставили. Он всё это время был вторым-третьим с нулём ошибок, а тестировали не его.
Повторить
Стенд полностью в репозитории: github.com/haiodo/reverse-proxy-bench.
./gencerts.sh && ./build.sh && ./runall.sh && python3 parse.pyСырые данные этого прогона лежат в baselines/2026-08-23/, все находки по конфигурированию - в FINDINGS.md. Прогоните через год и сравните - собственно, для этого я его и оформил отдельно.