Scheduling Tasks in the Cloud: AWS EventBridge, GitHub Actions, and Heroku Scheduler
Running scheduled jobs in the cloud is more nuanced than setting up a cron tab on a server. Cloud schedulers have their own syntax quirks, timezone handling, and failure modes. This guide covers the three platforms developers encounter most: AWS EventBridge, GitHub Actions scheduled workflows, and Heroku Scheduler.
Parse any cron expression: Get a plain-English explanation and the next 5 run times before you deploy a schedule.
Open Cron Parser βTable of Contents
AWS EventBridge (CloudWatch Events)
AWS EventBridge supports two expression types: cron expressions and rate expressions. Rate expressions are simpler for common intervals; cron expressions give you precise control.
# Rate expressions (simple)
rate(5 minutes)
rate(1 hour)
rate(7 days)
# Cron expressions β AWS uses 6 fields (adds year)
# Format: cron(minute hour day-of-month month day-of-week year)
cron(0 9 * * ? *) # every day at 9 AM UTC
cron(0 8 ? * MON-FRI *) # weekdays at 8 AM UTC
cron(0 0 1 * ? *) # first of month at midnight UTC
Important AWS-specific differences: the ? character means "no specific value" and must be used in either day-of-month or day-of-week (never both with a value, never both as *). Day names use three-letter abbreviations (MON, TUE, WED, THU, FRI, SAT, SUN). All times are UTC β if you need to run at a local business time, calculate the UTC equivalent.
Timezone support: EventBridge Scheduler (the newer service, distinct from EventBridge Rules) supports timezone-aware schedules directly. Use Scheduler rather than Rules for new scheduled workloads.
GitHub Actions Scheduled Workflows
GitHub Actions uses standard 5-field cron syntax. All schedules run in UTC.
# .github/workflows/daily-job.yml
on:
schedule:
- cron: '0 9 * * 1-5' # 9 AM UTC, MondayβFriday
jobs:
scheduled-task:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python scripts/daily_report.py
Important caveats: GitHub Actions scheduled workflows are not guaranteed to run at exactly the specified time β they may be delayed by up to 15β30 minutes during high-load periods. Do not use them for time-sensitive operations. Workflows scheduled during periods when your repository has no activity may be disabled by GitHub after 60 days β add a manual trigger to keep them alive.
# Add manual trigger to prevent auto-disable
on:
schedule:
- cron: '0 9 * * 1-5'
workflow_dispatch: # allows manual trigger via GitHub UI
Heroku Scheduler
Heroku Scheduler is the simplest of the three: a web UI where you set a job to run every 10 minutes, every hour, or every day at a specific time. There's no cron syntax β just dropdowns. All times are UTC.
Heroku Scheduler's key limitation is that it only supports three intervals. For more complex schedules (every Monday, first of month, etc.), you have two options: run a job every 10 minutes and check the time inside the script, or use a custom clock process with a proper cron library.
# Custom clock process for Heroku (Procfile)
clock: python clock.py
# clock.py using APScheduler
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job('cron', day_of_week='mon-fri', hour=9, timezone='UTC')
def weekday_report():
generate_report()
@scheduler.scheduled_job('cron', day=1, hour=0, timezone='UTC')
def monthly_cleanup():
cleanup_old_records()
scheduler.start()
Other Cloud Platforms: GCP and Azure
Google Cloud Scheduler supports both unix-cron syntax (5 fields) and App Engine cron syntax. It integrates natively with Cloud Run jobs, Cloud Functions, and Pub/Sub topics. Schedules are timezone-aware β specify a timezone directly rather than calculating UTC offsets manually.
# GCP Cloud Scheduler cron (standard 5-field syntax)
# Run a Cloud Run job every weekday at 9 AM Eastern
gcloud scheduler jobs create http daily-report \
--schedule="0 9 * * 1-5" \
--time-zone="America/New_York" \
--uri="https://your-region-project.cloudfunctions.net/daily-report" \
--http-method=POST
Azure Logic Apps and Azure Functions (timer trigger) both support cron-like scheduling. Azure uses a 6-field format similar to AWS: {second} {minute} {hour} {day} {month} {day-of-week}. Note the second field at position 0 β this differs from standard unix cron.
Making Scheduled Jobs Idempotent
Cloud schedulers can and do fire jobs more than once β due to retries, infrastructure issues, or daylight saving time edge cases. Design every scheduled job to be idempotent: running it twice produces the same result as running it once.
# Not idempotent β double-run creates duplicate records
INSERT INTO daily_summaries (date, value)
SELECT DATE(NOW()), SUM(amount) FROM orders WHERE DATE(created_at) = DATE(NOW());
# Idempotent β upsert with conflict handling
INSERT INTO daily_summaries (date, value)
SELECT DATE(NOW()), SUM(amount) FROM orders WHERE DATE(created_at) = DATE(NOW())
ON CONFLICT (date) DO UPDATE SET value = EXCLUDED.value;
Error Alerting for Scheduled Jobs
Scheduled jobs fail silently by default β there's no user to see the error. Set up explicit alerting so failures are noticed quickly. At minimum: send a notification to a Slack channel or email on job failure, and consider a "heartbeat" monitoring service (Cronitor, Healthchecks.io, Dead Man's Snitch) that alerts if the job doesn't check in within an expected window.
