Upstream Request Timeout: что значит и как безопасно повторить запрос
Разбираем значение Upstream Request Timeout, определяем слой обрыва и настраиваем безопасный повтор без двойной операции.
Содержание
Upstream Request Timeout: что значит и как безопасно повторить запрос
Upstream Request Timeout переводится как «шлюз не дождался ответа вышестоящего сервиса за отведённое время». Это не доказывает, что модель недоступна: лимит мог истечь в клиенте, reverse proxy, API gateway или на участке между gateway и upstream-моделью.
Правильный порядок — определить слой обрыва по исключению, HTTP status, времени и идентификатору запроса, а затем менять только соответствующий timeout. Увеличение всех лимитов сразу скрывает причину и может привести к повтору уже принятой операции.
Если запрос шёл через BetterToken, до retry откройте Dashboard и сопоставьте время, модель, status и расход Token. Это сразу отделяет запрос, дошедший до API, от сбоя до gateway и снижает риск повторить неизвестный результат.
Где происходит обрыв: фазы сетевого таймаута
Сетевой запрос к API больших языковых моделей проходит через несколько независимо ограниченных этапов:
[Пул клиента] --- Pool wait ---> [Свободное соединение]
[Клиент] --- Connect / Write ---> [API Gateway]
[Gateway] --- Upstream wait ---> [Модель]
[Клиент] <--- Read / Stream --- [Чанки ответа]
[Приложение] --- Общий deadline ---> [Вся операция]
- Connect timeout ограничивает установление DNS/TCP/TLS-соединения.
- Write timeout действует, пока клиент отправляет очередную часть тела запроса.
- Read Timeout (таймаут чтения): Время ожидания ответа сервера или очередного чанка в SSE-потоке.
- Pool Timeout (таймаут пула): Ожидание свободного сокета в пуле соединений при высокой конкурентности.
- Общий deadline приложения ограничивает всю операцию, а upstream timeout принадлежит gateway или провайдеру. Изменение одного таймера не продлевает остальные.
Диагностическая матрица таймаутов
| Симптом / Исключение | Фаза сбоя | Вероятная причина | Инженерное решение |
|---|---|---|---|
httpx.ConnectTimeout, HTTP-ответа нет | DNS, TCP или TLS | Маршрут, сертификаты, proxy или исходящий firewall | Повторить из того же окружения и сравнить DNS, CA и proxy до изменения connect timeout |
httpx.ReadTimeout до или между чанками | Client read/idle | За время таймера не пришёл следующий чанк | Сравнить время до первого байта и между чанками, проверить idle/read timeout промежуточных proxy |
| Есть HTTP status и тело ошибки | Gateway или upstream | Запрос дошёл до HTTP-сервера | Сохранить status, body и request ID; разбирать контракт ошибки, а не сеть подключения |
httpx.PoolTimeout | Пул клиента | Не освободилось соединение | Измерить конкурентность и загрузку пула; менять лимиты только после подтверждения насыщения |
| Отмена всегда через одинаковое общее время | Deadline приложения | Операцию остановил caller, job runner или reverse proxy | Найти владельца deadline и сопоставить его со всеми нижележащими таймерами |
Приоритетный порядок диагностики
- Соберите сигнал до повтора. Запишите начало и конец, класс исключения, HTTP status и body, request ID, факт получения хотя бы одного чанка, модель, endpoint и окружение запуска.
- Проверьте клиент и сеть. Повторите тест из того же контейнера или сервера. Если другое окружение работает, сравните DNS, CA-сертификаты, proxy-переменные и исходящий firewall.
- Проверьте промежуточные узлы. Сопоставьте read timeout клиента с idle/read timeout корпоративного и reverse proxy. Увеличение SDK timeout не продлит более короткий лимит proxy.
- Отделите gateway от upstream. Наличие HTTP-ответа означает, что сервер ответил. Используйте status, body и request ID, чтобы не смешивать upstream timeout, 429 и ошибку подключения.
- Меняйте одну переменную. Оставьте неизменными модель, запрос, сеть, endpoint и proxy — иначе сравнение не покажет причину.
Настройка гранулярных таймаутов в Python (httpx)
HTTPX документирует отдельные connect, read, write и pool timeout. Значения ниже — пример для контролируемого теста, а не универсальная production-рекомендация; реальные лимиты определяют по измеренным фазам запроса и общему deadline приложения.
import os
import httpx
from openai import OpenAI
API_KEY = os.environ.get("BETTERTOKEN_API_KEY", "your_api_key_here")
custom_timeout = httpx.Timeout(
connect=5.0, # Пример: установка сокета и TLS
read=120.0, # Пример: максимальная пауза между чанками
write=10.0, # Пример: ожидание отправки очередного чанка
pool=10.0 # Пример: ожидание соединения из пула
)
http_client = httpx.Client(
timeout=custom_timeout,
limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)
)
client = OpenAI(
base_url="https://www.bettertoken.ai/v1",
api_key=API_KEY,
http_client=http_client
)
response = client.chat.completions.create(
model="YOUR_CURRENT_MODEL_ID",
messages=[
{"role": "system", "content": "You are a senior systems architect."},
{"role": "user", "content": "Design a high-throughput distributed message broker."}
],
stream=True
)
for chunk in response:
delta = chunk.choices[0].delta.content or ""
print(delta, end="", flush=True)
Безопасный retry на всём жизненном цикле SSE
После отправки запроса считайте timeout неизвестным результатом, пока обратное не подтверждено:
- Не повторяйте автоматически частично полученный stream. Повтор может создать вторую генерацию и дополнительный расход; сохраните частичный результат и сначала сопоставьте первый запрос.
- Экспоненциальная задержка (Full Jitter): Повтор запроса при сбое подключения должен происходить с нарастающей случайной паузой.
- Идемпотентность только с серверной поддержкой: локальный ID задачи помогает сопоставить логи, но не предотвращает повторную генерацию. Автоматический повтор допустим только при документированной поддержке idempotency key и возможности сверить статус запроса; иначе неизвестный результат нужно сначала сверить с журналом использования.
import time
import random
import httpx
def stream_with_safe_retry(send_request, outcome_known_not_accepted, max_attempts=3):
"""send_request() возвращает context-managed iterable response."""
base_delay = 1.0
for attempt in range(max_attempts):
received_any = False
try:
with send_request() as response:
for chunk in response:
received_any = True
yield chunk
return
except (httpx.ConnectTimeout, httpx.ReadTimeout,
httpx.RemoteProtocolError) as err:
# Сюда попадают и ошибки во время итерации SSE.
if (received_any or attempt == max_attempts - 1
or not outcome_known_not_accepted(err)):
raise
sleep_time = random.uniform(0, base_delay * (2 ** attempt))
time.sleep(sleep_time)
outcome_known_not_accepted должен опираться на документированное поведение сервера или внешнюю проверку статуса. Клиентский timeout сам по себе не доказывает, что запрос не принят. Ограничьте и число попыток, и общий deadline.
Два теста, которые подтверждают исправление
- Короткий контрольный запрос. Запросите небольшой ответ и запишите status, время до первого байта, общую длительность, request ID и завершение stream. Если тест не проходит, сначала проверяйте сеть, аутентификацию и endpoint.
- Контролируемый длинный запрос. После успеха короткого теста увеличьте только ожидаемый объём ответа или верните исходную нагрузку. Не меняйте модель, endpoint, сеть и proxy. Если ломается только этот тест, сравните read/idle timeout, общий deadline и лимиты промежуточных узлов.
Исправление подтверждено, когда короткий тест и один повтор исходного сценария завершаются с ожидаемым status и результатом без необъяснимого дубля. Если запрос дошёл до BetterToken, сопоставьте в Dashboard время, модель, status и расход Token, затем откройте API Reference и проверьте контракт endpoint перед изменением retry.
Если сервер вернул 429, используйте отдельный разбор лимитов и Retry-After. Если соединение устанавливается, но обрывается именно поток событий, продолжите с диагностикой SSE-стриминга.