aws eventbridge cloud

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:

bash
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 value is exactly 1, you must use the singular form: rate(1 minute)
  • When value is greater than 1, you must use the plural form: rate(5 minutes)
  • Using rate(1 minutes) or rate(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:

bash
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.

bash
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
bash
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 ScheduleExpressionTimezone parameter 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.

bash
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

bash
cron(0 2 * * ? *)

2. Weekday Business Report at 8 AM UTC

bash
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:

bash
cron(0 23 1 * ? *)     -- 11 PM on the 1st (processes prior month's data)

4. Quarterly Reporting — First Day of Each Quarter

bash
cron(0 6 1 1,4,7,10 ? *)

5. Health Check Every 5 Minutes

bash
rate(5 minutes)

6. Lambda Warm-Up Every 15 Minutes

bash
rate(15 minutes)

7. Weekly Team Digest — Every Monday at 7 AM UTC

bash
cron(0 7 ? * MON *)

8. Third Friday of Each Month at 4 PM UTC (Options Expiry Pattern)

bash
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

bash
-- 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

bash
-- INVALID
rate(1 minutes)
rate(5 minute)

-- VALID
rate(1 minute)
rate(5 minutes)

3. Forgetting the Year Field

bash
-- 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

text
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

text
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

yaml
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

yaml
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

Explore related resources

Related Guides