AWS EventBridge cron — the limits and quirks AWS docs gloss over
The ? rule, Rules vs Scheduler, the DLQ everyone forgets, and why your daily Lambda is suddenly a $40/month line item.
Verified by maintainer·Last updated
EventBridge is two products that share a console tab. EventBridge Rules is the original, ships since 2014, and is what most existing infrastructure-as-code uses. EventBridge Scheduler launched late 2022 and is what new schedules should use. The docs are not always clear about which one is which, and the differences matter.
Six fields, year mandatory, ? mandatory
AWS cron is six fields wrapped in cron(...):
cron(minute hour day-of-month month day-of-week year)
The year field is required. Use * for "every year", a single value (2027) for a one-off, or a range (2026-2030) to scope a job to a fixed period.
The ? rule is the field where new AWS users get burned. Exactly one of day-of-month and day-of-week must be ?. Never both. Never neither.
cron(0 9 ? * MON-FRI *)— valid. Day-of-month is?.cron(0 9 15 * ? *)— valid. Day-of-week is?.cron(0 9 ? * ? *)— invalid. Both?.cron(0 9 * * MON-FRI *)— invalid. Neither?. The API rejects with a ValidationException at create time.
Day-of-week is 1-based, not 0-based
AWS uses 1 = Sunday, 2 = Monday, ..., 7 = Saturday. A Unix 1-5 (Mon–Fri) becomes AWS 2-6. When porting from a Linux crontab, this is the single most common silent bug — the cron looks right but it fires on the wrong days.
rate() is usually better than cron()
For interval schedules, rate(5 minutes) beats cron(*/5 * * * ? *) on readability. Use rate() when expressing "every N minutes/hours/days"; use cron() only when you need a specific wall-clock alignment (top of the hour, business hours, end of month).
Note: rate(N units) measures from creation time, not from a wall-clock anchor. A rate(1 day) schedule created at 14:23 fires at 14:23 every day, not at 00:00.
EventBridge Scheduler vs EventBridge Rules
The differences that matter:
- Timezone. Scheduler supports
--schedule-expression-timezone Europe/Berlin. Rules is UTC-only. If you want your job to fire at "09:00 in users' local time", you need Scheduler. - One-shot schedules. Scheduler supports
at(...)for single-execution jobs at a specific instant. Rules does not. - Flexible time windows. Scheduler can spread invocations across a window to smooth load (
FlexibleTimeWindow.Mode = FLEXIBLE). Useful if you have hundreds of schedules and don't want them all firing at exactly the same instant. - Dead-letter queues. Both support DLQ, but Scheduler's is per-schedule.
- Quotas. Scheduler scales to millions of schedules; Rules is capped at ~300 per region per account by default.
New work goes to Scheduler. There is no real downside.
The DLQ everyone forgets
The default behavior on target failure is "log a metric, drop the event, move on". Your Lambda errored? EventBridge does not retry indefinitely; after the configured retries (default 185 attempts over 24 hours), the event is lost unless you have a dead-letter queue configured.
Configure a DLQ on every schedule that matters. The cost of an SQS queue holding 50 failure events per month is approximately zero. The cost of discovering that the nightly financial close has been silently failing for two weeks is much higher.
Cost — the line item nobody plans for
EventBridge invocations are billed per event:
- EventBridge Rules: $1.00 per million events (rule matches).
- EventBridge Scheduler: $1.00 per million invocations.
That sounds free until you add the downstream costs. A */1 * * * ? * (every-minute) schedule that triggers a Lambda for a year is 525,600 invocations. The schedule itself is ~$0.50. The Lambda invocations add up. A 1-second Lambda at 128 MB is ~$1.30/month at every-minute frequency. A 30-second job at 1 GB is ~$80/month. Multiply by however many schedules you have.
When in doubt, ask whether you actually need every-minute frequency. Often you don't.
IAM — the role needs both ends
A schedule has a target (a Lambda, an ECS task, an SQS queue, an SNS topic). The IAM role on the schedule must grant permission to invoke that target. The Lambda you are targeting must also have a resource-based policy allowing EventBridge to invoke it.
For Lambda targets, the CLI does not auto-create the permission. You need to call aws lambda add-permission explicitly:
aws lambda add-permission \ --function-name my-nightly-rollup \ --statement-id eventbridge-invoke \ --action lambda:InvokeFunction \ --principal scheduler.amazonaws.com \ --source-arn arn:aws:scheduler:eu-west-1:ACCOUNT:schedule/default/my-schedule
Forgetting this is the second most common "why isn't my schedule firing" ticket after the ? rule.
The IAC trap
CloudFormation, Terraform, and CDK all model EventBridge Rules and EventBridge Scheduler as separate resources (AWS::Events::Rule vs AWS::Scheduler::Schedule). Mixing them in the same stack is fine; replacing one with the other is a destroy-then-create operation, which loses any in-flight retries. Plan migrations as additive (create the Scheduler version, point traffic at it, delete the Rule afterwards).