Railway.app is a fantastic platform for deploying full-stack apps and APIs. But when it comes to scheduled tasks and cron jobs, it has some specific constraints worth knowing before you commit your production automation to it.
This guide covers:
- How Railway's Cron Jobs actually work under the hood
- The real limitations (pricing, cold starts, minimum intervals)
- A working code example for a Railway scheduled task
- When to use Railway vs. a dedicated Python cron service
How Railway Cron Jobs Work
Railway implements scheduled tasks as Cron Services — a dedicated service type in your project. You define a cron expression and a command, and Railway spins up a container, runs the command, and shuts it down.
Here is how you set one up. First, create a Procfile or railway.toml:
# railway.toml
[deploy]
startCommand = "python scheduler.py"
[[services]]
name = "cron"
[services.cron]
cronSchedule = "0 9 * * *" # Every day at 9:00 AM UTC
command = "python jobs/daily_report.py"
Your Python script just needs to be a regular Python file:
# jobs/daily_report.py
import requests
import os
from datetime import date
def run_daily_report():
api_key = os.environ.get("REPORTING_API_KEY")
today = date.today().isoformat()
response = requests.get(
"https://api.yourservice.com/reports/generate",
headers={"Authorization": f"Bearer {api_key}"},
params={"date": today}
)
data = response.json()
print(f"Report generated: {data['report_id']} — {data['rows']} rows")
if __name__ == "__main__":
run_daily_report()
Real Limitations of Railway Cron Jobs
1. Minimum interval is 1 minute
Railway uses standard cron syntax, so the finest granularity is 1 minute (* * * * *). You cannot run jobs more frequently than once per minute.
2. You pay for a Worker even when idle
Railway's Cron Service requires a Worker — which means you're paying for a running process even between executions. At Railway's pricing of ~$0.000463/vCPU-minute, a continuously running 0.5 vCPU worker costs ~$10/month, even if your cron only fires for 30 seconds a day.
3. Cold start latency on Hobby plan
On the Railway Hobby plan, if your service is inactive, it may have a cold start delay of 5–20 seconds before your cron actually executes. For time-sensitive jobs this can be a problem.
4. No built-in job logs per-execution
Railway shows service logs in aggregate. There's no per-execution run history, so debugging a job that ran yesterday at 3am requires filtering through a wall of combined output.
5. No retry logic
If your Railway cron command exits with a non-zero code, Railway won't retry it. You have to implement retry logic yourself inside your script.
When Railway Cron Jobs Make Sense
Railway cron is a great choice when:
- You already have a Railway project and want to add a simple scheduled task without introducing a new service
- Your job runs infrequently (daily, weekly) so the idle Worker cost is negligible
- Your job is part of a bigger service — like a Django management command that shares a database connection from your existing Railway Postgres
When to Use a Dedicated Python Cron Service
If your automation is primarily a standalone Python script — not tightly coupled to a Railway-hosted database or API — a dedicated cron service is a better fit:
| Feature | Railway Cron | LiteLambda Cron |
|---|---|---|
| Minimum interval | 1 minute | 1 minute |
| Cost model | Always-on Worker (~$10/mo) | Per-execution (pay only when running) |
| pip packages | Via your Dockerfile | Per-job requirements.txt |
| Execution history | Aggregate logs | Per-run log dashboard |
| Retry on failure | Manual | Built-in (configurable retries) |
| Failure alerts | None built-in | Email + Telegram |
| Cold starts | Yes (Hobby plan) | No |
A Complete Example: Scraping + Alerting Cron on LiteLambda
Here is the same daily report job written for LiteLambda's handler(event, context) pattern:
import requests
import os
from datetime import date
def handler(event, context):
"""
Runs daily at 9:00 AM UTC.
Schedule: 0 9 * * *
"""
api_key = os.environ.get("REPORTING_API_KEY")
today = date.today().isoformat()
print(f"Generating daily report for {today}...")
response = requests.get(
"https://api.yourservice.com/reports/generate",
headers={"Authorization": f"Bearer {api_key}"},
params={"date": today},
timeout=30
)
response.raise_for_status()
data = response.json()
print(f"Report generated: {data['report_id']} — {data['rows']} rows")
return {
"status": "success",
"report_id": data["report_id"],
"rows": data["rows"]
}
The key difference: your function's return value is stored as the execution's return value in LiteLambda's run history. You can see exactly what each run produced, without digging through log aggregations.
Migrating from Railway Cron to LiteLambda
- Copy your Python script into LiteLambda's code editor
- Rename your entry point to
def handler(event, context): - Paste your Railway environment variables into LiteLambda's Environment Variables section
- Copy your cron expression (the syntax is identical — standard 5-field cron)
- Add any pip packages to the requirements field
That's it. Your Railway Worker cost drops to zero for that service, and you get per-run logs and built-in retry logic in exchange.