August 6, 2026 7 min read
How to monitor Railway cron jobs properly
Railway provides no built-in alerts for missed or failed cron executions. Here's how to detect missed runs, hung jobs, and silent failures in Railway cron services — from outside the platform.
Railway's cron service is one of the simplest ways to run scheduled jobs in a containerised environment. You configure a cron expression on any service, Railway starts the container on schedule, your code runs and exits, and Railway handles the rest.
What Railway does not do is tell you when something goes wrong.
Railway's own documentation is explicit about this. If a previous execution is still running when the next scheduled run is due, Railway skips the new run — silently. No alert fires. The execution log shows the previous run as still active, and the skipped run simply doesn't appear. From outside the platform, a hung cron and a skipped cron look identical: nothing happened.
Beyond silent skips, Railway fires no native alert for a missed run caused by a deployment issue, a container that fails to start due to a missing environment variable, or a service that exits non-zero. You can see failures in the Railway dashboard if you go looking — but nothing proactively notifies you.
Railway's specific failure modes
Silent skip due to overlap. Railway does not automatically terminate a previous execution when the next scheduled time arrives. If your job takes longer than its schedule interval — a daily job that starts taking 26 hours, a 5-minute job that hangs on a database lock — Railway skips subsequent runs indefinitely until the stuck run is manually stopped or times out. Nothing alerts you that this is happening.
Container fails to start. A misconfigured environment variable, a missing dependency in the Docker image, or a Railway deployment issue can prevent the container from starting. The scheduled time passes, nothing runs, and no external alert fires.
Exit code non-zero. Railway logs the failure in the deployment view. No notification is sent unless you have configured Railway's built-in notification integrations, which cover deployment failures broadly but are not specific to cron run outcomes.
Exit code 0 with no useful output. Railway considers a zero exit code a successful run. A job that exits 0 after processing zero records, syncing nothing, or writing an empty file is indistinguishable from a successful run in Railway's view.
The fix: HTTP pings to an external monitor
Push-based heartbeat monitoring solves all of these failure modes. Your cron container sends HTTP pings to an external service when it starts and when it finishes. The external service holds the schedule configuration and fires an alert if the expected pings don't arrive — regardless of what Railway is doing internally.
Here's the complete pattern for a Node.js Railway cron service:
import { CrontifyMonitor } from '@crontify/sdk';
const monitor = new CrontifyMonitor({
apiKey: process.env.CRONTIFY_API_KEY!,
monitorId: process.env.CRONTIFY_MONITOR_ID!,
});
async function main(): Promise<void> {
await monitor.wrap(async () => {
const result = await runJob();
return {
meta: {
records_processed: result.count,
duration_ms: result.durationMs,
}
};
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Set CRONTIFY_API_KEY and CRONTIFY_MONITOR_ID as Railway environment variables in your service settings.
For a Python Railway cron service:
import os
import sys
import requests
import traceback
MONITOR_ID = os.environ["CRONTIFY_MONITOR_ID"]
API_KEY = os.environ["CRONTIFY_API_KEY"]
BASE_URL = f"https://api.crontify.com/api/v1/ping/{MONITOR_ID}"
HEADERS = {"X-API-Key": API_KEY}
def ping(event: str, payload: dict | None = None) -> None:
try:
requests.post(f"{BASE_URL}/{event}", headers=HEADERS,
json=payload, timeout=10)
except Exception:
pass
def main() -> None:
ping("start")
try:
result = run_job()
ping("success", {"meta": {"records_processed": result.count}})
except Exception as exc:
ping("fail", {"message": str(exc), "log": traceback.format_exc()})
sys.exit(1)
if __name__ == "__main__":
main()
Detecting Railway's silent skip specifically
Railway's silent skip is the most insidious failure mode because it leaves no trace. The job simply doesn't run and nothing surfaces the absence.
External monitoring catches this because the monitoring service knows when the ping was expected. Configure your monitor in Crontify with:
- Expected schedule: the same cron expression as your Railway service
- Grace period: 10–15 minutes — Railway schedules are not guaranteed to the minute
- Max duration: slightly above your job's p95 runtime
If no start ping arrives within the grace period after the expected time, Crontify fires an alert. If a start ping arrives but no success ping follows within the max duration — the hung job scenario that causes Railway's silent skip — a hung job alert fires.
Silent failure detection for data jobs
Railway marks your service as successful when it exits 0. Crontify evaluates what the job actually accomplished.
The metadata attached to the success ping is evaluated against alert rules you define in the dashboard:
records_processed eq 0→ fire alertfiles_synced eq 0→ fire alertapi_calls_made eq 0→ fire alert (upstream was never reached)
A Railway cron that exits 0 after processing nothing will still trigger an alert. This is the failure mode Railway has no mechanism to detect at any level.
Configuring the monitor
In Crontify:
- Create a new monitor
- Set the expected schedule to match your Railway cron expression
- Set the grace period (10–15 minutes for Railway)
- Configure alert channels — Slack, email, Discord, or webhook
- Add alert rules on output metadata if your job processes data
- Copy the monitor ID into your Railway service's environment variables
Crontify is free for up to 5 monitors — no credit card required.
Start monitoring your scheduled jobs
Free plan includes 5 monitors. No credit card required. Up and running in under 5 minutes.
Get started free →More from the blog
August 11, 2026 7 min read
How to get alerts when pg_cron jobs fail
pg_cron logs failures to cron.job_run_details but sends no external alert. Here's how to add missed run detection, failure alerts, and silent failure detection to PostgreSQL scheduled jobs.
Read more →
July 28, 2026 8 min read
Cron job idempotency: why it matters and how to implement it
A cron job that runs twice should produce the same result as one that ran once. Most don't. Here's what idempotency means for scheduled jobs, why it's non-negotiable in production, and how to implement it correctly.
Read more →