Skip to content

[Bug]: Gateway proxy_read_timeout is hardcoded to 300s, truncating slow long-context streaming responses #4239

Closed

Description

@r4victor

Steps to reproduce

  1. Deploy a long-context model as a service on a single GPU, e.g. Qwen/Qwen3.8-27B-FP8 on one H100 with --max-model-len 262144 --max-num-seqs 16, behind a dstack gateway.
  2. Send ~30 concurrent streaming chat-completion requests, each with 60k-100k input tokens.
  3. Wait 5 minutes.

Actual behaviour

At 300s the gateway closes connections mid-body. The client sees a truncated chunked response rather than an HTTP status code:

ClientPayloadError("Response payload is not completed:
  <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")

Timing matched exactly: requests dispatched at 15:41:52, connection cut at 15:46:59.

Nothing was failing upstream. For the whole preceding 300s the load generator reported errors=0, and 22 of the 30 requests had already returned successfully. The remaining requests were simply queued behind ~1.9M tokens of prefill.

The mechanism is that proxy_read_timeout bounds the gap between successive reads from upstream, not total request duration. For a streaming LLM response the engine sends response headers immediately and the first data: chunk only after prefill completes, so this timeout effectively caps time-to-first-token. Under load with 100k-token prompts, TTFT can exceed 300s legitimately. For example, I hit it during benchmark warmup phase that fired ~30 concurrent 100k-token requests at once. Steady-state serving rarely leaves a request queued for five minutes, so this is not a common availability problem, but it may still happen.

Expected behaviour

proxy_read_timeout should be configurable per service, the way client_max_body_size already is via DEFAULT_SERVICE_CLIENT_MAX_BODY_SIZE and a per-service field.

A higher default for services that declare model: would also be reasonable, since long-context inference is exactly the workload where a 300s inter-read gap is reachable without anything being wrong.

dstack version

0.21.3 (master, e828c9e14)

Additional information

Activity

  1. mikemikimike commented on Sep 1, 2026

    @mikemikimike
  2. self-assigned this
    on Sep 29, 2026
  3. r4victor commented on Sep 30, 2026

    @r4victor
    CollaboratorAuthor

    The problem is more critical with the model entrypoint (gateway.<gateway-domain>), where requests fail after 60s rather than 300s.

    The gateway app forwards entrypoint requests to the service's nginx site with an httpx client whose timeout is DSTACK_SERVICE_CLIENT_TIMEOUT, 60s by default:

    if service.domain is not None:
    # Forward to Nginx so that requests are visible to StatsCollector in the access log
    return httpx.AsyncClient(
    base_url="http://127.0.0.1",
    headers={"Host": service.domain},
    timeout=HTTP_TIMEOUT,
    )

    httpx applies the read timeout per socket read, same as proxy_read_timeout. A streaming request that waits more than 60s for its first token fails with 502 Error requesting model: ReadTimeout(''), and so does a non-streaming request whose generation takes more than 60s. The variable is read from the gateway app's environment, which users can't configure, so on gateways the limit is effectively fixed at 60s.

    HTTP_TIMEOUT = environ.get_int("DSTACK_SERVICE_CLIENT_TIMEOUT", default=60)
    # Same as default Nginx proxy timeout; override via DSTACK_SERVICE_CLIENT_TIMEOUT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions