Railway is what you choose when you don't want to do ops. Deploys are one push, databases are one click, and the dashboard shows you metrics and logs. Which is why the gap surprises people: Railway does not tell you when your app goes down. The health check you configured? It runs at deploy time, to decide whether the new deployment is ready to receive traffic. After that, nothing is watching.
This guide sets up real monitoring for a Railway project: external HTTP checks on every service, heartbeats for cron jobs, Railway deploy events flowing in as incidents, a public status page, and (if you want it) automatic restarts when a service hangs. Most of it takes one pasted token.
Railway's health check runs once, at deploy time
Railway's healthcheck documentation is explicit: the healthcheck endpoint is used to verify that a new deployment is ready before traffic is cut over to it. It is a deployment gate, not continuous monitoring. If your service OOMs on Tuesday night, deadlocks under load, or starts returning 500s because a downstream API changed, Railway's healthcheck has no opinion. It already did its job at deploy time.
Railway's built-in observability (metrics, logs) is useful for debugging, but it's pull-based: someone has to be looking at it. There are no uptime checks, no incident lifecycle, and no alerting on runtime downtime. Everything after a successful deploy is invisible unless you add an external watcher.
The 60-second setup: paste a project token, import your services
upti.my has a native Railway integration, so the fastest path is not creating checks by hand. In Railway, create a project token (Project → Settings → Tokens). Project tokens are scoped to one project and one environment, revocable any time in Railway's UI, and grant no account-wide access, which is exactly the trust model you want for a third-party tool.
Then in upti.my:
- Paste the token. upti.my validates it and shows a preview: your Railway project's name and every service in the environment, with its public URL and current deployment status. Nothing is stored until you confirm.
- Tick the services you want monitored. Services with a public domain are pre-checked with the URL pre-filled (you can append a path like
/health). Workers and databases without a public URL are listed as "no public URL — self-heal only". More on those below, because they stop being blind spots in a minute. - Confirm. upti.my creates an HTTP healthcheck per selected service and offers to wire up a self-healing workflow for them (that part is a Growth feature; the monitoring itself is free).
What the integration stores
The Railway connect, service discovery, monitor import, deploy-event ingestion, and status pages are all available on the free plan (10 checks, 5 heartbeats, 1 status page, which covers a typical Railway project).
Make the checks assert more than a 200
The import creates standard HTTP checks, which means you get the full configuration surface: probe from multiple continents, require confirmation from more than one node before an incident opens (this kills cold-start and single-region false positives), and most importantly, validate the response body, not just the status code.
A Railway app that lost its database connection often still returns something with a 200 on its root route. Assert on content: a version string, a "status": "ok" field from a real health endpoint that checks its dependencies. We wrote up the full argument in HTTP 200 is not a health check.
App sleeping: inbound pings do NOT keep Railway awake
A myth worth killing, because most monitoring guides get it backwards: Railway's app sleeping is based on outbound traffic, not inbound. An external monitor pinging your app every minute does not keep it awake, and a sleeping app woken by your monitor's probe will answer slowly on the first request.
Two sane configurations:
- Production services: disable app sleeping. A customer-facing service that cold-starts on first request isn't saving you money, it's costing you users.
- Services you deliberately let sleep: raise the check timeout and require 2+ node confirmations, so wake-up latency doesn't page you at 4 a.m. for a service behaving exactly as configured.
Cron jobs: Railway silently skips runs
Railway's cron scheduler has a documented behavior that surprises everyone the first time: if the previous execution of a scheduled service is still running when the next run is due, the run is skipped. No error, no log entry for the run that never happened. From the outside, a hung job and a skipped job look identical: silence.
HTTP checks can't see this. There's nothing to probe. The fix is a heartbeat (dead man's switch): the job pings a unique URL as its final act, and upti.my alerts when the ping doesn't arrive on schedule.
#!/usr/bin/env bash
set -euo pipefail
# ... your actual job ...
python sync_invoices.py
# Last line: ping the heartbeat ONLY if everything above succeeded
curl -fsS "https://heartbeats.upti.my/v1/monitors/<your-token>"Set the expected period to the cron schedule and a grace period longer than the job's worst-case runtime. If the run is skipped, hangs, or crashes, the ping never arrives and you get paged. The dedicated Railway cron monitoring guide covers telling skipped from hung runs with Job Chains, and how to monitor cron jobs properly covers grace-period sizing and output validation.
Deploy events: incidents for the services no ping can reach
Here's where the native integration earns its keep. Those "no public URL" workers and databases from the import step, and the crash-loops that recover between polling intervals: HTTP checks structurally cannot see them. Railway can, because it pushes project webhooks on every deployment state change.
On the integration card in upti.my, click Webhook URL, copy it, and paste it into Railway → Project → Settings → Webhooks. From that moment, deploy events flow into the same incident engine as your HTTP checks:
railway.deployment.crashedopens a high-severity incident on the affected service, even if it's a worker with no URL, and even if the HTTP check looks fine.railway.deployment.failedopens a medium-severity incident (a failed build didn't take the old deploy down).railway.deployment.successresolves the incident.- Crash-loop detection: three crashes within ten minutes escalate the incident to critical, with a timeline note. This is the failure mode where a service crashes, restarts, and crashes again fast enough that polling never catches it red-handed.
Repeated crashes deduplicate into one incident instead of a page storm, and these events are filterable in workflows like any other source. As far as we know, no other uptime vendor treats Railway deploy events as first-class incident signals.
Route alerts without noise
Detection is half the job; the other half is not hating your alerting two weeks later. On the free and Starter plans you get pre-built workflow templates. The "service down alerts" template routes incidents to Slack, Discord, or email with deduplication so a flapping check doesn't fire twenty notifications.
Plan gating, stated plainly
If you're routing to a team, read how to reduce alert fatigue with smarter routing before you point every incident at everyone's phone.
Is Railway down, or is it just you?
When your app stops responding, work the decision tree from the outside in:
- Check Railway's status page (status.railway.com). Platform-wide incidents show up there.
- Check your external monitor's multi-region view. If probes fail from every continent but Railway's status is green, it's your app, so go look at deploy events and logs. If only one region fails, it's network weather, and confirmation thresholds should have kept you from being paged at all.
- If Railway itself is down, your job is communication. Your status page, hosted off-platform, is how customers find out you know. A status page deployed on the same platform that's down is a punchline, not a status page.
Close the loop: auto-restart a dead Railway service
Some failures have the same fix every time: OOM thrash, deadlocks, a hung event loop. Restart it. For that class, you can take the human out of the loop entirely. The Railway integration ships a Self-Heal workflow node: when an incident opens on a monitored service, it restarts the latest deployment (or triggers a full redeploy) through Railway's API, with guardrails on by default: a cooldown window, a per-incident action cap, and a full audit trail of every action taken or skipped.
The import flow will even offer to build the workflow for you: trigger on incident, filter to your imported monitors, heal the matching service, notify you about what it did. This uses custom workflows, so it requires Growth (14-day trial). The full walkthrough, including the guardrails and the DIY GraphQL alternative, is in Auto-Restart a Railway Service When It Goes Down.
A status page that stays up when Railway doesn't
Every check you imported can be a component on a public status page hosted at {your-slug}.upti.my, on infrastructure independent from your Railway project, which is the entire point. One click from the checks you already created, on the free plan.
When an incident is live, what you write matters as much as the red dot. See what to put on a status page during an incident.
The whole setup, recapped
- Create a Railway project token, paste it into upti.my, and import your services as HTTP checks (about a minute).
- Tighten the checks: response-body assertions, multi-node confirmation, timeouts tuned for app sleeping if you use it.
- Add a heartbeat to every cron job (curl as the last line).
- Paste the webhook URL into Railway settings so crashes and failed deploys become incidents, including for workers with no URL.
- Route alerts through the service-down template with dedupe.
- Publish the status page.
- Optional, Growth: enable the self-heal workflow so restarts happen without you.
And once it's configured, version it. The uptimyctl CLI exports your applications, healthchecks, tags, and alert rules as config you can diff and re-import (heartbeat monitors are managed in the app):
# Snapshot applications, healthchecks, tags, and alert rules
uptimyctl export -f monitoring-config.json
# Recreate or promote it later
uptimyctl import -f monitoring-config.json📌Key Takeaways
- 1Railway's healthcheck is a deploy gate, not monitoring. After deploy, nothing watches your app
- 2The native integration turns setup into: paste a project token, tick your services, done
- 3App sleeping is outbound-based: external pings don't keep Railway apps awake
- 4Railway silently skips cron runs when the previous run is still going. Heartbeats catch what logs can't
- 5Webhook deploy events make workers and crash-loops first-class incidents, no public URL required
- 6Connect, discovery, import, deploy events, and status pages are free; Self-Heal workflows are Growth
Railway made deploying a solved problem. Monitoring it is now one pasted token. Start with the free plan and your project is covered before your coffee cools.