Uptime Monitoring for Railway Apps (Free, Multi-Region)
Railway is a deployment platform that makes it genuinely fast to go from code to running container. You push, it builds, it deploys. No Kubernetes YAML, no Dockerfile wrestling (usually), no CloudFormation.
But Railway does not send you an alert when your service goes down. That part is on you.
Here's how to add health checks and external uptime monitoring to a Railway-deployed service.
What breaks on Railway and why you need monitoring
Sleeping services — Railway's hobby plan sleeps services after 10 minutes of inactivity (on the legacy free tier). The first request after sleep takes 5–30 seconds to cold-start. Your monitoring will catch this as a timeout, which is exactly what it is.
Container restarts — Railway will restart a crashing container, but there's a window between crash and restart where requests return 503. Without monitoring, you only know this happened if a user complains.
Build failures on redeploy — If a new build fails, Railway rolls back to the last good deploy. But if you had a bad migration that ran before the rollback, your database might be in an inconsistent state. A health check that verifies database connectivity will catch this faster than your users will.
Environment variable gaps — Railway's service-to-service references (${{Postgres.DATABASE_URL}}) are evaluated at deploy time. If a referenced service restarts and changes its URL, your app may still be running with a stale connection string.
Step 1: Add a health endpoint
For a Node.js service:
// src/routes/health.ts
import { Router } from 'express'
import { Pool } from 'pg'
const router = Router()
// Railway sets DATABASE_URL automatically for Postgres services
const pool = process.env.DATABASE_URL
? new Pool({ connectionString: process.env.DATABASE_URL })
: null
router.get('/health', async (req, res) => {
const checks: Record<string, any> = {}
// Check Postgres if configured
if (pool) {
const start = Date.now()
try {
await pool.query('SELECT 1')
checks.database = { status: 'ok', latencyMs: Date.now() - start }
} catch (err: any) {
checks.database = { status: 'error', error: err.message }
}
}
// Add Redis check if configured (Railway Redis service)
if (process.env.REDIS_URL) {
const { createClient } = await import('redis')
const redis = createClient({ url: process.env.REDIS_URL })
const start = Date.now()
try {
await redis.connect()
await redis.ping()
await redis.disconnect()
checks.redis = { status: 'ok', latencyMs: Date.now() - start }
} catch (err: any) {
checks.redis = { status: 'error', error: err.message }
}
}
const allOk = Object.values(checks).every((c: any) => c.status === 'ok')
res.status(allOk ? 200 : 503).json({
status: allOk ? 'ok' : 'degraded',
service: process.env.RAILWAY_SERVICE_NAME ?? 'unknown',
environment: process.env.RAILWAY_ENVIRONMENT_NAME ?? 'unknown',
checks,
uptime: process.uptime(),
})
})
export default router
Railway automatically injects RAILWAY_SERVICE_NAME and RAILWAY_ENVIRONMENT_NAME, so your health response tells you exactly which environment is being probed.
Step 2: Wire it up in your main app file
// src/app.ts
import express from 'express'
import healthRouter from './routes/health'
const app = express()
app.use(healthRouter)
// ... rest of your routes
app.listen(process.env.PORT || 3000)
Railway sets PORT automatically — always use process.env.PORT, not a hardcoded value.
Step 3: Add a Procfile or healthcheck command
Railway supports a HEALTHCHECK in your Dockerfile:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1
This tells Railway itself to restart the container if the health check fails three times in a row. It's separate from external monitoring — the container check handles restarts, the external monitor handles alerting you.
Step 4: Get the public URL
Railway generates a public domain for each service. Find it in the Railway dashboard under Settings → Networking → Public Networking. It looks like your-service.up.railway.app.
Step 5: Set up external monitoring
Your Dockerfile HEALTHCHECK only restarts the container. It doesn't tell you when it restarted or how long it was down. You need an external monitor for that.
- Go to vigilmon.online — free tier, no credit card.
- Create an HTTP(S) monitor.
- URL:
https://your-service.up.railway.app/health - Interval: 60s
- Expected status: 200
- Regions: pick two or more for multi-region coverage
- Alerts: email or Slack webhook
Step 6: Handle sleeping services (hobby plan)
If you're on Railway's hobby plan with sleeping enabled, configure your monitor to allow a 30-second timeout for the first probe after a sleep period. You'll still get alerted if the service fails to wake within that window, but you won't get false positives every time it sleeps.
Alternatively, use the monitoring pings themselves as keep-alive heartbeats: a 60-second monitor interval is enough to prevent Railway from putting the service to sleep.
Sample healthy response
{
"status": "ok",
"service": "api",
"environment": "production",
"checks": {
"database": { "status": "ok", "latencyMs": 12 },
"redis": { "status": "ok", "latencyMs": 3 }
},
"uptime": 86412.7
}
Sample degraded response (503)
{
"status": "degraded",
"service": "api",
"environment": "production",
"checks": {
"database": { "status": "error", "error": "connect ECONNREFUSED 127.0.0.1:5432" },
"redis": { "status": "ok", "latencyMs": 2 }
},
"uptime": 86412.7
}
Recap
- Add a
/healthendpoint that checks Postgres and Redis and returns structured JSON. - Use
RAILWAY_SERVICE_NAMEandRAILWAY_ENVIRONMENT_NAMEin your response — alerts will tell you which environment is failing. - Add a
HEALTHCHECKto your Dockerfile so Railway auto-restarts unhealthy containers. - Point a free external monitor at your public Railway URL — vigilmon.online.
- On the hobby plan, a 60s monitoring interval doubles as a keep-alive that prevents the service from sleeping.
Railway handles the deploy pipeline. You handle knowing when the running service has a problem.
Top comments (0)