[FIXED] Healthcheck "service unavailable" on all new deploys, app returns 200
azkron
PROOP

3 months ago

All new deployments fail healthcheck with "service unavailable" even though the application responds HTTP 200 to /health internally.

Symptoms:

  • App starts, listens on 0.0.0.0:3000, responds 200 to /health (visible in deploy logs)
  • Railway's healthcheck prober simultaneously reports "service unavailable" for every attempt
  • Retries exhaust the full window and deploy fails

Deploy logs (truncated):

Starting Healthcheck
Path: /health
Retry window: 1m40s

Attempt #1 failed with service unavailable. Continuing to retry for 1m29s
Attempt #2 failed with service unavailable. Continuing to retry for 1m27s
timestamp=... level=INFO message="Listening on http://0.0.0.0:3000"
timestamp=... message="Sent HTTP response" http.status=200 http.method=GET http.url=/health
timestamp=... message="Sent HTTP response" http.status=200 http.method=GET http.url=/health
Attempt #3 failed with service unavailable. Continuing to retry for 1m25s
Attempt #4 failed with service unavailable. Continuing to retry for 1m21s
...
Deploy failed

What I've tried (all produce the same result):

  • railway up (CLI upload)
  • railway redeploy (build from GitHub repo)
  • Removing Docker HEALTHCHECK instruction from Dockerfile
  • Removing PORT env var (let Railway inject it)
  • Setting PORT=3000 explicitly
  • Increasing healthcheckTimeout to 300s in railway.toml
  • Disabling Cloudflare proxy (DNS only)
  • Removing the auto-generated Railway domain
  • Deleting the service and creating a brand new one

What works:

  • Redeploying a previously successful deployment from the dashboard (old cached image)

Setup:

  • Node.js 24.13.1-alpine, Effect Platform, Prisma ORM
  • PORT=3000, app listens on 0.0.0.0:3000
  • Custom domain with target port 3000
  • railway.toml: healthcheckPath = "/health", healthcheckTimeout = 300

This looks identical to the issue in this thread which was caused by conflicting firewall rules and required a platform-side fix.

Solved

2 Replies

azkron
PROOP

3 months ago

Found the issue, my CORS middleware was crashing on requests with no Origin header. When you use a specific allowlist (not *), the origin matcher function receives undefined for same-origin or server-to-server requests and throws. Adding a null check before matching fixed it.


3 months ago

Glad you found it. CORS origin matchers receiving undefined for requests without an Origin header (like our healthcheck prober) is a subtle one. Thanks for sharing the fix.


Status changed to Awaiting User Response Railway 3 months ago


Status changed to Solved brody 3 months ago


Welcome!

Sign in to your Railway account to join the conversation.

Loading...