HerokuPaaSCloudScheduling

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.

9 min read·Updated April 2026

Platform Comparison

PlatformCron SupportFree TierStrength
HerokuBasic (fixed intervals only)No (Eco dynos)Simple setup, one-click
RenderFull standard cronNo (0.1 CPU free tier limited)Full cron syntax, managed infra
RailwayFull standard cronTrial $5 creditGitOps-first, cron config in YAML
Fly.ioVia 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.wsgi

All 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