Skip to content

Failed to stream build logs in CI when build queue is non-empty (zero-logs failure mode not covered by #743) #868

Description

@frankjrobinson

Summary

railway up (foreground) fails with Failed to stream build logs: Failed to retrieve build log from CI runners when Railway's build queue is non-empty. PR #743 (v4.44.0) fixed the case where the stream errored after receiving log lines (treats it as success). But there is a second failure mode — stream dies before any log line is received — which #743's if received_any_logs { return Ok(()) } doesn't cover.

Environment

  • CLI: @railway/cli@4.44.0 (latest), confirmed against the v4.44.0 source at src/controllers/deployment.rs:158
  • GitLab CI shared runners → AWS NAT → Railway (Google Cloud)
  • Foreground railway up (no --detach)
  • Backend service with a longer Dockerfile (40+ COPY layers, multi-stage Python builder)

Reproduction

Run railway up from CI during peak load (EU business hours seems to correlate). Same project off-peak runs cleanly.

Observed timing comparison

Same project, same Dockerfile, same CLI version (4.44.0):

Off-peak success (UTC 22:13)

22:13:45  Build Logs: <url>
22:14:22  exporting to docker image (build complete = 37s)
22:15:08  Job succeeded

Peak-hour failure (UTC 14:19)

14:19:36  Build Logs: <url>
14:20:43  Failed to stream build logs: Failed to retrieve build log  (67s of total silence)

The 67s window contained zero log lines — no individual build step output was received before the WebSocket subscription died. This indicates the build hadn't begun emitting logs yet (still queued / picking up an executor) and the idle WebSocket got reaped — most likely by AWS NAT idle timeout (60–90s typical for non-keepalive TCP).

Why PR #743 doesn't cover this

#743 added two things:

  1. tokio::time::sleep(Duration::from_millis(500)).await; — a 500ms initial delay before subscribing
  2. if received_any_logs { return Ok(()) } — treat stream closure as success if data already came through

Neither fixes the queue-wait case. The 500ms delay is finite; the queue can be 30s+. And received_any_logs is false when no logs ever arrive, so the early-return doesn't fire and the loop just retries 12× into the same NAT-killed connection.

Suggested fixes (any one would be sufficient)

  1. Client-side WebSocket keepalive ping — send a ping every ~30s while waiting for the first log line. Keeps NAT entries alive across queue waits. Smallest change in scope.
  2. Server-side keepalive frames on the GraphQL build-logs subscription, even before the build starts producing real logs.
  3. Treat "queued, no logs yet, connection dropped" specifically — if the deployment status is still BUILDING / QUEUED when the WebSocket dies, retry the subscription instead of failing. Caller exit code stays 0 since the deploy is genuinely in flight.

Workaround

railway up --detach skips the subscription entirely. The deploy proceeds normally; only the build-log tail is missed. We've shipped a wrapper in our CI that tries streaming first and falls back to acknowledging the in-flight deploy if the stream dies after the upload landed (i.e., the Build Logs: URL was already printed in the output). Happy to share if helpful.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions