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:
tokio::time::sleep(Duration::from_millis(500)).await; — a 500ms initial delay before subscribing
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)
- 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.
- Server-side keepalive frames on the GraphQL build-logs subscription, even before the build starts producing real logs.
- 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
Summary
railway up(foreground) fails withFailed to stream build logs: Failed to retrieve build logfrom 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'sif received_any_logs { return Ok(()) }doesn't cover.Environment
@railway/cli@4.44.0(latest), confirmed against the v4.44.0 source at src/controllers/deployment.rs:158railway up(no--detach)Reproduction
Run
railway upfrom 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)
Peak-hour failure (UTC 14:19)
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:
tokio::time::sleep(Duration::from_millis(500)).await;— a 500ms initial delay before subscribingif received_any_logs { return Ok(()) }— treat stream closure as success if data already came throughNeither fixes the queue-wait case. The 500ms delay is finite; the queue can be 30s+. And
received_any_logsisfalsewhen 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)
BUILDING/QUEUEDwhen the WebSocket dies, retry the subscription instead of failing. Caller exit code stays 0 since the deploy is genuinely in flight.Workaround
railway up --detachskips 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., theBuild Logs:URL was already printed in the output). Happy to share if helpful.Related