AWS EventBridge: Cron vs Rate Expressions — When to Use Each
by Sinthuyan Arulselvam · April 6, 2026
EventBridge schedules sit at the operational core of most AWS architectures — nightly database snapshots, weekday report pipelines, end-of-month billing runs. Before you write a single line of Terraform, you need to choose between two fundamentally different scheduling primitives: rate expressions and cron expressions. Getting this choice wrong is silent and dangerous: your schedule will run, just not when you expect it to.
Rate vs Cron — The Fundamental Difference
A rate expression defines a fixed interval. It answers the question: how often? It starts ticking from the moment the rule is enabled and fires every N minutes, hours, or days from that anchor point. There is no concept of a calendar — no awareness of Monday versus Sunday, no concept of midnight, no knowledge of months.
A cron expression defines a calendar position. It answers the question: when? It fires at a precise moment in time — 09:00 UTC every weekday, the first day of every quarter, once a year on a specific date. The schedule is anchored to the clock, not to when the rule was created.
The practical decision rule is simple: if the human description of your schedule contains the words "every N minutes" or "every N hours," use a rate expression. If it contains any calendar concept — "weekdays," "Monday," "the 15th," "9 AM," "monthly" — use cron.
Rate Expression Syntax
Rate expressions follow a rigid two-token format:
rate(value unit) Where value is a positive integer and unit is one of minute, minutes, hour, hours, day, or days. The singular/plural rule is a common source of rejected schedules:
- When
valueis exactly1, you must use the singular form:rate(1 minute) - When
valueis greater than1, you must use the plural form:rate(5 minutes) - Using
rate(1 minutes)orrate(5 minute)will cause a validation error at creation time
The minimum supported interval is rate(1 minute). Sub-minute scheduling is not supported by EventBridge Rules or EventBridge Scheduler. If you need sub-minute precision, you need a different tool (SQS delay queues, Step Functions wait states, or an application-level timer).
Representative rate expressions:
rate(1 minute) -- every 60 seconds
rate(5 minutes) -- every five minutes
rate(1 hour) -- every 60 minutes
rate(12 hours) -- twice a day (but not at fixed clock times)
rate(1 day) -- every 24 hours from rule creation A critical caveat with rate(1 day): if you enable the rule at 14:37 UTC on a Tuesday, it will fire every day at 14:37 UTC. If you want the job to run at midnight, use a cron expression. Rate expressions have no concept of "the start of the day."
EventBridge Cron Expression Fields
EventBridge cron expressions have six fields, not five. Standard Unix/Linux cron uses five fields; AWS adds a mandatory sixth field for the year. Forgetting this distinction is one of the most common migration mistakes when moving cron definitions from a Linux server to EventBridge.
cron(minutes hours day-of-month month day-of-week year) | Field | Position | Allowed Values | Wildcards |
|---|---|---|---|
| Minutes | 1 | 0–59 | * , - / |
| Hours | 2 | 0–23 | * , - / |
| Day-of-month | 3 | 1–31 | * , - / ? L W |
| Month | 4 | 1–12 or JAN–DEC | * , - / |
| Day-of-week | 5 | 1–7 or SUN–SAT | * , - / ? L # |
| Year | 6 | 1970–2199 | * , - / |
The year field accepts * for "every year," a specific year like 2027, a range like 2026-2028, or a list like 2026,2027. For recurring schedules, always use * in the year field.
The ? Constraint — DOM vs DOW Mutual Exclusion
This is the rule that catches almost every first-time EventBridge cron author. You cannot specify a non-wildcard value for both day-of-month and day-of-week simultaneously. Exactly one of the two must be ? (the "no specific value" token).
The reason is mathematical: specifying both creates an ambiguous intersection that EventBridge refuses to resolve. Consider cron(0 9 15 * MON *) — does this mean "9 AM on the 15th" or "9 AM on Mondays"? Both? Only when the 15th falls on a Monday? EventBridge rejects the ambiguity at the API level.
The rule:
- If you want to schedule by day of month (e.g., the 1st, the 15th), set DOM to your value and DOW to
? - If you want to schedule by day of week (e.g., Monday, weekdays), set DOW to your value and DOM to
? - For "every day," use
*for DOM and?for DOW, or vice versa — but not wildcards for both
cron(0 0 1 * ? *) -- midnight on the 1st of every month (DOM, DOW=?)
cron(0 9 ? * MON-FRI *) -- 9 AM on weekdays (DOW, DOM=?)
cron(0 0 * * ? *) -- midnight every day (DOM=*, DOW=?) UTC and Timezone Handling
EventBridge Rules are UTC-only. There is no timezone parameter on a rule's schedule expression. Every time you write in a cron or rate expression, it is interpreted as UTC. If your business requirement is "9 AM New York time," you need to perform the UTC offset yourself: 9 AM EST is 14:00 UTC; 9 AM EDT (summer) is 13:00 UTC.
This creates a DST (Daylight Saving Time) problem. A rule set to cron(0 14 ? * MON-FRI *) will run at 9 AM EST in winter but at 10 AM EST in summer when New York switches to EDT. There is no clean solution for this with EventBridge Rules. Your options are:
- Accept the one-hour shift — acceptable for non-time-sensitive jobs
- Maintain two rules — one for EST season, one for EDT season, each enabled/disabled by a Lambda that runs at the DST transition
- Use EventBridge Scheduler — Scheduler supports a
ScheduleExpressionTimezoneparameter and handles DST automatically
If timezone correctness matters to your stakeholders, EventBridge Scheduler is the right product for the job.
One-Time Scheduling with the Year Field
The year field enables a pattern that standard cron cannot express: a schedule that runs exactly once, on a specific future date, and then never again. This is valuable for compliance cutoffs, product launches, one-off database migrations, and license expiry processing.
cron(0 0 1 1 ? 2027) -- midnight on January 1st, 2027, only
cron(0 6 15 3 ? 2027) -- 6 AM on March 15th, 2027, only
cron(30 23 31 12 ? 2026) -- 11:30 PM on December 31st, 2026, only After the year passes and the schedule fires (or doesn't, if the date is invalid for that year), the rule continues to exist but will never trigger again. Best practice: pair one-time schedules with an automatic disable or delete via a triggered Lambda, or use EventBridge Scheduler's one-time at() expression instead, which is purpose-built for this pattern.
Production Examples — 8 Real-World Patterns
1. Nightly Database Backup at 2 AM UTC
cron(0 2 * * ? *) 2. Weekday Business Report at 8 AM UTC
cron(0 8 ? * MON-FRI *) 3. End-of-Month Billing Run (Last Day is Tricky)
EventBridge does not support the L special character in the same way Quartz does. Use the 28th as a safe proxy for "end of month" across all months, or trigger on the 1st of the next month instead:
cron(0 23 1 * ? *) -- 11 PM on the 1st (processes prior month's data) 4. Quarterly Reporting — First Day of Each Quarter
cron(0 6 1 1,4,7,10 ? *) 5. Health Check Every 5 Minutes
rate(5 minutes) 6. Lambda Warm-Up Every 15 Minutes
rate(15 minutes) 7. Weekly Team Digest — Every Monday at 7 AM UTC
cron(0 7 ? * MON *) 8. Third Friday of Each Month at 4 PM UTC (Options Expiry Pattern)
cron(0 16 ? * 6#3 *) The # operator means "the Nth occurrence of this weekday in the month." 6#3 is the 3rd Friday (day 6 in AWS's 1=SUN numbering).
EventBridge Rules vs EventBridge Scheduler
AWS has two distinct products for scheduled invocations. They are often confused because both use cron and rate syntax, but they serve different use cases and have meaningfully different capabilities.
| Feature | EventBridge Rules | EventBridge Scheduler |
|---|---|---|
| Timezone support | UTC only | Full IANA timezone support |
| One-time schedules | Via year field (workaround) | Native at() expression |
| DST handling | Manual (two rules) | Automatic |
| Target types | 20+ AWS services | 270+ API targets |
| Flexible time windows | No | Yes (invoke within a window) |
| Dead-letter queue | Yes (on event bus) | Yes (per schedule) |
| Retry policy | Limited | Configurable per schedule |
| Pricing model | Per invocation (after free tier) | Per invocation (separate free tier) |
| Best for | Event-driven + scheduled hybrid rules | Pure scheduling workloads |
When to use Rules: Your rule is already filtering on event patterns from other AWS services (e.g., S3 PutObject events) and you're just adding a schedule alongside it. Or your team is deeply familiar with the EventBridge console and doesn't need DST handling.
When to use Scheduler: You're building a new scheduled workload from scratch. You need timezone support, flexible time windows (invocations can happen within a time window rather than at an exact moment, useful for distributing load), or you need to target an API action that isn't available as an EventBridge Rules target.
Common Mistakes
1. Both DOM and DOW Are Non-Wildcards
-- INVALID: Both fields specify values
cron(0 9 15 * MON *)
-- VALID: Pick one
cron(0 9 15 * ? *) -- 9 AM on the 15th
cron(0 9 ? * MON *) -- 9 AM on Mondays 2. Rate Singular/Plural Mismatch
-- INVALID
rate(1 minutes)
rate(5 minute)
-- VALID
rate(1 minute)
rate(5 minutes) 3. Forgetting the Year Field
-- INVALID: Only 5 fields -- this is Linux cron syntax
cron(0 9 ? * MON-FRI)
-- VALID: Six fields with * for year
cron(0 9 ? * MON-FRI *) 4. UTC Confusion Causing Off-By-One-Hour Bugs
A rule set to run at 0 9 ? * * * and expected to fire at 9 AM Sydney time (AEDT, UTC+11) will actually fire at 8 PM Sydney time. Always convert your local business time to UTC before writing the expression, and document the original timezone intent in a comment or tag on the rule.
5. Using rate(1 day) as a Midnight Substitute
Rate expressions are relative to rule creation time, not the clock. A rule enabled at 4 PM will fire every subsequent day at 4 PM. Use cron(0 0 * * ? *) for a guaranteed midnight execution.
Terraform and CloudFormation Examples
Terraform — EventBridge Rule
resource "aws_cloudwatch_event_rule" "nightly_backup" {
name = "nightly-backup"
description = "Trigger nightly DB backup at 02:00 UTC"
schedule_expression = "cron(0 2 * * ? *)"
state = "ENABLED"
tags = {
Schedule = "cron(0 2 * * ? *)"
Timezone = "UTC"
}
}
resource "aws_cloudwatch_event_target" "nightly_backup_target" {
rule = aws_cloudwatch_event_rule.nightly_backup.name
target_id = "NightlyBackupLambda"
arn = aws_lambda_function.backup.arn
}
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.backup.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.nightly_backup.arn
} Terraform — EventBridge Scheduler with Timezone
resource "aws_scheduler_schedule" "weekday_report" {
name = "weekday-morning-report"
group_name = "default"
flexible_time_window {
mode = "OFF"
}
schedule_expression = "cron(0 9 ? * MON-FRI *)"
schedule_expression_timezone = "America/New_York"
target {
arn = aws_lambda_function.report.arn
role_arn = aws_iam_role.scheduler_invoke.arn
retry_policy {
maximum_retry_attempts = 3
maximum_event_age_in_seconds = 3600
}
}
} CloudFormation — Rule with Rate Expression
Resources:
HealthCheckRule:
Type: AWS::Events::Rule
Properties:
Name: health-check-every-5-minutes
ScheduleExpression: rate(5 minutes)
State: ENABLED
Targets:
- Id: HealthCheckLambda
Arn: !GetAtt HealthCheckFunction.Arn
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref HealthCheckFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt HealthCheckRule.Arn CloudFormation — Scheduler One-Time Execution
Resources:
MigrationSchedule:
Type: AWS::Scheduler::Schedule
Properties:
Name: one-time-data-migration
ScheduleExpression: "at(2027-03-01T02:00:00)"
ScheduleExpressionTimezone: UTC
FlexibleTimeWindow:
Mode: "OFF"
Target:
Arn: !GetAtt MigrationFunction.Arn
RoleArn: !GetAtt SchedulerRole.Arn Key Takeaways
- Use rate for interval-based workloads with no calendar requirement; use cron for anything calendar-aware
- EventBridge cron has six fields — the year field is mandatory and has no Linux equivalent
- Exactly one of DOM or DOW must be
?— this is non-negotiable and enforced at the API level - All EventBridge Rules are UTC-only; use EventBridge Scheduler when you need IANA timezone support and DST handling
- For one-time events, prefer Scheduler's
at()expression over the year field workaround - Always tag your rules with the original timezone intent and the human-readable schedule description — six months later,
cron(0 14 ? * MON-FRI *)tells you nothing about why 14:00 was chosen