Heroku Scheduler & PaaS Cron Guide
Modern PaaS platforms — Heroku, Render, Railway, and Fly.io — each have their own approach to running scheduled tasks. This guide covers how to set up cron jobs on each platform, their scheduling syntax, and key gotchas.
Contents
Platform Comparison
| Platform | Cron Support | Free Tier | Strength |
|---|---|---|---|
| Heroku | Basic (fixed intervals only) | No (Eco dynos) | Simple setup, one-click |
| Render | Full standard cron | No (0.1 CPU free tier limited) | Full cron syntax, managed infra |
| Railway | Full standard cron | Trial $5 credit | GitOps-first, cron config in YAML |
| Fly.io | Via Machines (full control) | Yes (3 shared-cpu-1x free) | Most flexible, global deployment |
Heroku Scheduler
Heroku Scheduler is a free add-on that runs jobs at fixed intervals: every 10 minutes, every hour, or every day. It does NOT support full cron expressions — if you need a specific time or day-of-week, use a background worker with Celery Beat or APScheduler instead.
shell — add the add-on
# Add Heroku Scheduler to your app heroku addons:create scheduler:standard --app myapp # Open scheduler dashboard heroku addons:open scheduler --app myapp
In the scheduler dashboard, you set the command (e.g. python manage.py cleanup) and the frequency. The command runs in a one-off dyno — billed at your dyno rate.
Procfile — define rake/script tasks
web: gunicorn myapp.wsgi worker: celery -A myapp worker # Scheduler runs these as one-off dynos: # python manage.py send_digest # python scripts/cleanup.py
Heroku Scheduler Gotchas
- Runs at approximately the scheduled time — not guaranteed to the minute
- Limited to 10min / 1hr / 1day intervals — no specific times
- Each run spins up a new one-off dyno — cold start latency applies
- No retry on failure — implement your own retry logic in the command
- Free tier removed in 2022 — costs at least Eco dyno rate per run
Render Cron Jobs
Render supports cron jobs as a first-class service type. You get full 5-field cron expression syntax, and the job runs in a fresh container each time — no persistent state between runs.
render.yaml — Infrastructure as Code
services:
- type: cron
name: daily-report
env: python
schedule: "0 9 * * 1-5" # Every weekday at 9 AM UTC
buildCommand: pip install -r requirements.txt
startCommand: python scripts/send_report.py
- type: cron
name: db-cleanup
env: python
schedule: "0 2 * * *" # Every night at 2 AM UTC
buildCommand: pip install -r requirements.txt
startCommand: python manage.py cleanup_old_sessions
- type: web
name: myapp
env: python
buildCommand: pip install -r requirements.txt
startCommand: gunicorn myapp.wsgiAll Render cron times are in UTC. The job container has access to your environment variables set in the Render dashboard. Logs are retained and viewable per job run.
Render Cron Advantages
- Full standard 5-field cron expression support
- Zero-config — no add-ons required, just add to render.yaml
- Per-job logs in the dashboard
- Supports all runtimes: Python, Node, Go, Ruby, Docker
Railway Cron Services
Railway supports scheduled cron services alongside your web services. Configure them in the Railway dashboard or via railway.toml.
railway.toml
[build] builder = "NIXPACKS" [deploy] startCommand = "python scripts/sync.py" cronSchedule = "*/15 * * * *" # Every 15 minutes
Dashboard — Service Settings → Cron Schedule
# Examples (standard 5-field cron, UTC): 0 0 * * * # Daily at midnight 0 9 * * 1 # Every Monday at 9 AM */30 9-17 * * 1-5 # Every 30 min during business hours
Railway Cron Notes
- Cron schedule is set per service — one cron expression per service
- Service is stopped between runs — not a long-running process
- Usage-based billing: you only pay for CPU/memory during the run
- Combine with private networking to call internal Railway services
Fly.io Scheduled Machines
Fly.io uses Fly Machines — lightweight VMs that start and stop on demand. For scheduled tasks, you can either run a persistent Celery Beat worker or use the Machines API to schedule one-shot runs.
Option 1: Persistent Beat Worker
fly.toml
app = 'myapp' primary_region = 'iad' [processes] web = "gunicorn myapp.wsgi" beat = "celery -A myapp beat -l info" worker = "celery -A myapp worker -l info" [[services]] processes = ["web"] internal_port = 8000 # beat and worker processes run as additional Machines
Option 2: Machines API (one-shot runs)
shell — schedule a one-shot Machine via CLI
# Start a Machine, run a command, and auto-destroy on exit fly machine run . --app myapp --restart no --command "python manage.py cleanup" --env DATABASE_URL=$DATABASE_URL # Combine with system cron or an external scheduler # to trigger fly machine run on a schedule
For Fly.io, the most cost-effective pattern for infrequent cron jobs is to trigger a Machine via the API from an external cron (e.g. GitHub Actions scheduled workflow) and let it auto-destroy after the task completes. You only pay for the seconds the Machine runs.
Cross-Platform Best Practices
1. Always schedule in UTC
All four platforms (Heroku, Render, Railway, Fly.io) interpret cron times as UTC. Store UTC in your schedules and convert to local time only in your application's display layer.
2. Design tasks to be idempotent
PaaS schedulers can run a task more than once (e.g., after a platform outage recovery). Your task should produce the same result whether it runs once or three times — use database upserts, check a completion flag, or use idempotency keys.
3. Set timeouts and hard limits
Configure a maximum runtime for your cron job. Render and Railway kill jobs after a configurable timeout. For Heroku, set a timeout inside your script. A job that runs forever consumes dyno hours or credits indefinitely.
4. Log job start, end, and outcome
Write structured logs at the start and end of each run: {event: 'cron_start', job: 'db-cleanup', ts: '...'} and {event: 'cron_end', duration_ms: 1200, records_deleted: 45}. This makes debugging and SLA verification trivial.
5. Alert on failure
Connect your job runner to an alerting system (Sentry, PagerDuty, Slack webhook). Every platform exposes run status via logs — ship them to a log aggregator and set alert rules on error patterns.
6. Consider external scheduling for high-reliability use cases
For mission-critical schedules, use a dedicated cron-as-a-service (Inngest, Quirrel, or a self-hosted Temporal) rather than platform-level scheduling. Platform cron is best-effort with ~1-minute precision.
Validate your cron expressions
Test any cron expression before deploying to Heroku, Render, or Railway. See next run times, plain-English explanation, and platform compatibility.
Open Cron Debugger