Оптимизация TTFB в Nginx: где теряются миллисекунды
Высокий TTFB не всегда означает, что Nginx медленно отдаёт страницу. Разделяем задержку сети, TLS, прокси и приложения, настраиваем измерения и находим участок, который действительно съедает время.

TTFB в 800 мс не говорит, что Nginx восемьсот миллисекунд «формирует ответ». В это число могут попасть DNS, установка TCP-соединения, TLS, ожидание upstream-приложения и только затем работа самого Nginx. На Nginx 1.25.x и новее оптимизация начинается не с увеличения буферов, а с разбиения задержки на измеряемые этапы.
Сначала разделите TTFB на части
TTFB — время до получения первого байта ответа. Для пользователя это единый показатель, но на сервере он складывается из разных событий. Если сайт работает через HTTPS, клиент сначала устанавливает TCP-соединение, затем выполняет TLS-рукопожатие. После этого Nginx может ждать upstream — например, PHP-FPM, Gunicorn, Node.js или другой HTTP-сервис.
Важное различие: $request_time показывает полное время обработки запроса Nginx, а $upstream_header_time — сколько прошло до получения заголовков от upstream. Если upstream молчит 600 мс, настройка sendfile не сделает динамическую страницу быстрой.
Оптимизируйте не TTFB как одну цифру, а самый длинный измеренный участок цепочки: клиент — TLS — Nginx — upstream — база данных.
Upstream[1] — сервер, к которому Nginx обращается за ответом: приложение, PHP-FPM, другой Nginx или внутренний API.
Поставьте измерения до изменения конфигурации
Начните с двух независимых измерений. Первое показывает картину глазами клиента, второе — что происходило внутри Nginx. Запускайте тесты с одной и той же машины, с отключённым кешем клиента и на нескольких URL: статический файл, динамическая страница без кеша и страница после прогрева.
Проверка с curl
Команда ниже использует стандартные переменные curl. Она не заменяет измерение из браузера или внешнего узла, но помогает отделить соединение от ожидания первого байта.
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/catalog/item
Запустите команду несколько раз. Большой разрыв между connect и tls указывает на сеть или TLS, а разрыв между tls и ttfb — на ожидание ответа после установления соединения. Для HTTP без TLS поле tls будет равно нулю.
Лог времени на стороне Nginx
В серверном блоке добавьте отдельный формат логов. Переменные upstream заполняются только для запросов, которые действительно передавались upstream; для статических файлов они будут пустыми.
log_format timing '$remote_addr $request '
'status=$status bytes=$body_bytes_sent '
'request_time=$request_time '
'upstream_connect=$upstream_connect_time '
'upstream_header=$upstream_header_time '
'upstream_response=$upstream_response_time';
access_log /var/log/nginx/timing.log timing;
Проверьте конфигурацию командой nginx -t, затем перечитайте конфигурацию штатным способом вашей системы. После серии запросов сравните значения по URL и статусам. Один медленный запрос не объясняет проблему: смотрите медиану и высокий хвост, например p95 или p99, в вашей системе мониторинга.
| Наблюдение | Вероятный участок задержки | Что проверить |
|---|---|---|
Высокий TTFB, маленький upstream_header_time | Сеть, TLS или клиентский маршрут | Измерения с разных регионов, TLS, балансировщик, DNS |
Высокие upstream_connect_time и TTFB | Соединение между Nginx и приложением | Сеть, перегруженный upstream, число доступных workers |
Маленький connect, высокий upstream_header_time | Приложение или его зависимости | SQL, внешние HTTP-вызовы, блокировки, очередь запросов |
| Быстрые заголовки, долгое полное время ответа | Передача тела или генерация потока | Размер ответа, скорость клиента, буферизацию, сжатие |
| Медленно только при промахе кеша | Генерация origin-ответа | Кеширование, прогрев, инвалидация и долю MISS |
Где Nginx действительно может убрать задержку
1. Кешируйте ответы, которые можно кешировать
Если один и тот же публичный ответ каждый раз уходит в приложение, Nginx способен убрать ожидание upstream для повторных запросов. Но кеш нельзя включать вслепую для личных страниц, корзины, административных разделов и ответов с пользовательскими cookies.
proxy_cache_path /var/cache/nginx/site
keys_zone=site_cache:20m
max_size=1g
inactive=10m
use_temp_path=off;
server {
location / {
proxy_cache site_cache;
proxy_cache_methods GET HEAD;
proxy_cache_valid 200 30s;
proxy_cache_valid 404 5s;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_pass http://app_backend;
}
}
Это пример для публичного GET-трафика, а не универсальная настройка. В production сначала проверьте заголовки Set-Cookie, Cache-Control, авторизацию и правила очистки кеша. Короткий TTL уменьшает риск устаревших данных, но не устраняет промахи при резком росте трафика.
Добавьте диагностический заголовок только во внутренней среде или убедитесь, что он не раскрывает лишнюю информацию клиенту:
add_header X-Cache-Status $upstream_cache_status always;
Значения вроде HIT, MISS и BYPASS помогают понять, почему одинаковый URL не получает одинаковое время ответа. Инвалидация кеша — отдельная задача; здесь важна только проверка влияния попаданий на TTFB.
2. Не путайте буферизацию с ускорением первого байта
Для проксируемого ответа Nginx по умолчанию буферизует данные upstream. Директива proxy_buffering off может быть нужна для потоковых ответов, SSE или долгих загрузок, но сама по себе не ускоряет создание первого байта. Иногда она даже переносит больше работы на клиент и увеличивает чувствительность к медленной сети.
Изменять размеры proxy_buffers ради TTFB стоит только после подтверждения, что ответ упирается в буферизацию или запись во временный файл. Иначе вы потратите память workers, не тронув причину задержки. Для обычной HTML-страницы сначала проверяйте приложение и кеш.
3. Отделите статические файлы от динамики
Статический файл не должен проходить через PHP или другой application server. Для него используйте отдельный location, корректный root и длительное кеширование на клиенте, если имена файлов версионируются.
location /assets/ {
root /srv/www/site;
try_files $uri =404;
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
Этот фрагмент предполагает путь /srv/www/site/assets/.... После настройки проверьте статус, заголовки и отсутствие проксирования: в access log не должно появляться upstream-времени для этих файлов.
4. Проверьте TLS и keep-alive, но не ожидайте чуда
Повторное HTTPS-соединение обычно дешевле нового: клиент может использовать существующее TCP-соединение и TLS-сессию. Если каждый запрос открывает отдельное соединение, TTFB будет зависеть от RTT и стоимости рукопожатия. Проверяйте это с реального клиента, а не только локальным curl на сервере.
Параметры keepalive_timeout, HTTP/2 и TLS-сессии должны соответствовать балансировщику и клиентам. Слишком длинные keep-alive увеличивают число занятых соединений, слишком короткие заставляют клиентов чаще подключаться заново. Оптимальное значение нельзя вывести из одной цифры TTFB: измеряйте новые и повторные запросы отдельно.
Что ломается первым
Upstream отвечает быстро в среднем, но медленно в хвосте
Среднее значение скрывает очередь. Приложение может отвечать за 80 мс в обычном случае и за несколько секунд, когда workers заняты, база ждёт блокировку или внешний API не укладывается в таймаут. Сопоставьте upstream_header_time с количеством процессов приложения, временем SQL-запросов и лимитами соединений.
Не увеличивайте таймауты как способ «убрать» высокий TTFB. Так вы только дольше удержите соединения Nginx и upstream. Таймаут должен отражать допустимое время операции, а медленные зависимости нужно ограничивать отдельно.
Кеш ускоряет чтение, но создаёт устаревшие данные
Самый быстрый ответ — тот, который не пришлось генерировать. Цена — сложность корректного удаления кеша. Если после публикации товара пользователь видит старую цену, выигрыш в миллисекундах превращается в функциональную ошибку. Для каждого кешируемого URL заранее опишите TTL, условие обхода кеша и событие очистки.
Сжатие уменьшает трафик, но может задержать выдачу
Gzip или Brotli сокращают размер ответа, однако требуют CPU и могут менять момент отправки первых данных. Для крупного HTML сравнивайте не только размер, но и time_starttransfer, полное время и загрузку CPU. Для уже сжатых JPEG, PNG, WebP, ZIP и видео повторное сжатие обычно не даёт полезного результата.
Как проводить замер до и после
Фиксируйте условия: URL, метод, наличие cookies, регион клиента, HTTP-версию, размер ответа, состояние кеша, число параллельных запросов и версию Nginx. Без этого «после стало быстрее» нельзя воспроизвести.
- Снимите базовые значения curl и серверных логов для статического, динамического и кешируемого URL.
- Разделите запросы на HIT, MISS и BYPASS, если используется proxy cache.
- Измените одну настройку, проверьте
nginx -tи перечитайте конфигурацию. - Повторите тест после прогрева кеша и отдельно на холодном кеше.
- Сравните медиану и p95, а также ошибки 4xx, 5xx, загрузку CPU, память и число активных соединений.
Если локальный curl показывает 20 мс, а пользователи получают 400 мс, ищите разницу между точками измерения: CDN, балансировщик, мобильная сеть, DNS или TLS. Если обе точки видят высокий upstream_header_time, Nginx почти наверняка лишь честно ждёт приложение.
Что делать дальше
Сначала соберите распределение $request_time и $upstream_header_time по маршрутам, статусам и наличию кеша. Затем выберите один узкий эксперимент: например, кеширование публичного GET или устранение лишнего прохода статических файлов через upstream. На проде следите не только за TTFB, но и за p95/p99, долей cache HIT, временем upstream и числом ответов 502/504. Инвалидация кеша и оптимизация базы данных — смежные темы, их не стоит маскировать настройками Nginx.
[1] В конфигурации Nginx upstream может быть задан группой серверов в блоке upstream или указан напрямую в proxy_pass. В статье термин используется для любого сервиса, который формирует ответ за Nginx.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: Nginx documentation: ngx_http_log_module, Nginx documentation: ngx_http_proxy_module
Частые вопросы
Как понять, что TTFB тормозит именно Nginx?
Поможет ли увеличить proxy_buffers?
Безопасно ли включать proxy_cache для всего сайта?
Что делать, если после настройки TTFB не изменился?
Имеет ли значение версия Nginx?
Какой показатель смотреть в production?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.