{"index":9,"slug":"editorial-2027-10-practice-long-form-interview","title":"HTTP-таймауты: как разложить общий deadline на измеримые фазы","excerpt":"Запрос может завершиться таймаутом до ответа сервера или повторить запись после неясного результата. Разбираем deadline, фазы HTTP и проверку retry без догадок.","contentHtml":"

Запрос к API иногда отвечает за 900 мс, а клиент прекращает ждать через 800 мс. Пользователь видит ошибку, хотя сервер мог завершить операцию. Хуже случай с записью: клиент повторяет POST, не зная, успел ли первый запрос попасть в обработчик. Цена ошибки — потерянный результат, двойное изменение состояния и лишняя нагрузка на upstream.

Симптом не говорит, где потрачено время. Один общий timeout смешивает DNS, TCP, TLS, отправку тела, ожидание первого байта и чтение ответа. Повторная попытка получает новый полный бюджет и скрывает исходную причину. Тезис статьи прост: задайте один абсолютный deadline для операции, разложите его на наблюдаемые фазы и разрешайте retry только после проверки семантики метода.

Deadline — граница операции

Общий deadline отвечает на вопрос: до какого момента результат имеет смысл для вызывающего кода. Фазовый timeout отвечает на другой вопрос: сколько можно ждать конкретный переход. Если дать DNS, TCP, TLS и чтению по 800 мс каждому, запрос может жить несколько секунд. Если ограничить connect 50 мс, холодное разрешение имени превратит нормальный запрос в ложный отказ.

Храните абсолютный момент окончания или остаток бюджета. Перед каждой фазой вычисляйте remaining = deadline - now. Если остатка нет, завершайте операцию до следующего сетевого вызова. Новый независимый таймер после истечения deadline нарушает контракт вызывающего кода.

Механизм: что измерять

DNS показывает время разрешения имени. TCP connect — установление соединения. TLS handshake — защищённый обмен и проверку сертификата. Request write — передачу заголовков и тела. TTFB показывает ожидание первого байта, а read — получение остального ответа. Эти интервалы отвечают на разные вопросы, поэтому один счётчик total duration не заменяет их.

Фазы HTTP-запроса и сигналы
ФазаЧто измеряемТипичный симптомДействие
DNSРазрешение имениМедленный первый запросПроверить resolver, кэш и лимит DNS
TCP connectОткрытие соединенияОтказ до TLSПроверить маршрут, pool и connect timeout
TLS handshakeЗащищённый обменСоединение есть, ответа нетПроверить цепочку, crypto и TLS budget
Request writeОтправка телаЗависает uploadПроверить размер, backpressure и write timeout
TTFB/readОтвет и телоКлиент ждёт серверСопоставить server time, read timeout и payload

Симптом → причина → проверка → действие

СимптомПричинаПроверкаДействие
Timeout без статуса HTTPИстёк deadline до первого байта или клиент закрыл сокетСравнить timestamps DNS, connect, TLS, TTFB и server logРазделить фазовые интервалы и не называть отказ HTTP-статусом
Первый запрос медленный, следующие быстрыеCold DNS, TCP или TLS скрыты poolСравнить cold и warm соединенияИзмерять установление соединения отдельно от чтения
POST повторяет изменениеRetry запущен без доказанной идемпотентностиПроверить метод, operation-id и результат первого вызоваДобавить ключ дедупликации или сначала читать статус операции
Каждый retry ждёт полный timeoutПопытки не делят общий deadlineПосчитать абсолютный deadline всего запросаПередавать остаток бюджета, а не запускать новый полный таймер
В логах только total durationКлиент не экспортирует фазыПроверить API instrumentation и события poolФиксировать известную границу и не выдавать реконструкцию за факт

Повторная попытка не лечит неизвестный результат

Timeout не доказывает, что сервер ничего не сделал. Для GET повтор обычно допустим, если ресурс читает актуальное состояние и клиент принимает возможное изменение данных между попытками. Для POST, PUT и DELETE решение зависит от семантики операции. Нужны идемпотентность, ключ дедупликации или endpoint для проверки статуса. Одного сетевого флага retry недостаточно.

Три попытки по 800 мс дают до 2,4 секунды ожидания без учёта задержек и одновременно утроенную нагрузку на upstream. Общий deadline должен охватывать все попытки. Если остатка мало, новая попытка не должна начинать работу. Для записи безопаснее вернуть состояние «результат неизвестен» и проверить operation-id, чем отправлять запрос повторно наугад.

\"Карта
Общий бюджет проходит через фазы запроса. Диагноз должен ссылаться на сигнал конкретной фазы, а не только на слово timeout.

Учебный пример без сети

Функция ниже получает общий бюджет и длительности уже измеренных фаз. Она возвращает сумму, остаток и причину. Это учебный пример: он не открывает socket, не моделирует отмену запроса и не заменяет таймеры HTTP-клиента. Его граница — проверка арифметического инварианта отдельно от сети.

import { allocateTimeoutBudget } from './upgrade-2027-10.mjs';\n\nconst within = allocateTimeoutBudget({\n  totalMs: 800,\n  dnsMs: 42,\n  tlsMs: 88,\n  requestMs: 510,\n});\nconst late = allocateTimeoutBudget({\n  totalMs: 800,\n  dnsMs: 120,\n  tlsMs: 210,\n  requestMs: 560,\n});\n\nconsole.log(within.ok, within.remainingMs);\nconsole.log(late.ok, late.reason, late.remainingMs);\n// true 160\n// false deadline-exceeded 0

В первом вызове потрачено 640 мс, поэтому остаётся 160 мс. Во втором сумма равна 890 мс: функция возвращает deadline-exceeded и нулевой остаток. Реальный адаптер должен дополнительно отменять вложенные DNS, socket и чтение, иначе завершение функции не остановит работу сокета.

Порядок проверки

  1. Запишите абсолютный deadline и момент, когда клиент прекратил ожидание. Не начинайте диагностику с увеличения числа.
  2. Добавьте DNS, TCP, TLS, write, TTFB и read в одну структурированную запись. Одинаковые имена фаз важнее названий полей конкретной библиотеки.
  3. Проверьте, передаётся ли остаток бюджета на следующую фазу и между retry. Новый таймер не должен начинаться после истечения deadline.
  4. Сопоставьте фазу отказа с HTTP-методом. Для записи проверьте идемпотентность, operation-id и способ узнать результат первого вызова.
  5. Разделите connect, read и общий deadline в конфигурации. У каждого параметра должен быть владелец и тест на граничное значение.
  6. Сравните cold и warm соединение. Pool может скрыть TLS в одном случае и показать его в другом.
  7. Повторите проверку через реальный proxy и downstream. Локальная функция не доказывает поведение всей цепочки.

Что измерение не доказывает

Большой TTFB не доказывает, что сервер медленный: время могло уйти на proxy, очередь или повторный TLS. Малый TTFB не гарантирует быструю загрузку всего тела. Клиентский timeout не равен HTTP-статусу: клиент может закрыть соединение, а сервер продолжить обработчик. Метрики фаз нужно связывать с серверным временем и размером ответа, но не подменять ими друг друга.

Если библиотека отдаёт только total duration, реконструированные интервалы нельзя называть фактами. Выберите instrumented adapter или события connection pool. До этого фиксируйте только известную границу: «клиент прекратил ждать через N мс». Неполное, но честное измерение лучше точного на вид вымысла.

Ограничения и критерий готовности

Подход не учитывает jitter часов, системные очереди, HTTP/2 multiplexing и детали конкретного proxy. Учебные числа не являются нормативными значениями для вашей сети. RFC описывает протокол, но не выбирает таймауты, retry policy или параметры клиента. Для боевой системы нужен адаптер с отменой всех вложенных операций и отдельная проверка безопасности повторов.

Материал готов к применению, если интеграционный тест для выбранного endpoint показывает фазы на cold и warm соединении, общий deadline ограничивает все попытки, а просроченная операция останавливает вложенное чтение. Для записи дополнительно нужен проверяемый operation-id: после timeout система может узнать, был ли первый вызов принят. Если хотя бы одна граница не наблюдается, результат следует считать недоказанным, а не успешным.

Проверяемые источники

"}