cronuru
Guide

AWS EventBridge Scheduler — A Complete Guide

AWS has two cron-style scheduling services: the legacy EventBridge Rules (always UTC, simpler) and the newer EventBridge Scheduler (per-schedule timezones, flexible time windows, scales to millions of schedules). This guide covers both, plus the IAM, DLQ, and monitoring patterns you need for production.

Updated

Rules vs Scheduler

AWS has two distinct services for scheduled invocations:

EventBridge Rules (formerly CloudWatch Events). The original service. Each “rule” can be scheduled with a cron or rate expression and points at one or more targets. Always UTC. ~100 rules per region per account. Console: EventBridge → Rules.

EventBridge Scheduler. Released late 2022. Designed for scheduled-job-at-scale. Each “schedule” is a separate resource with its own IAM role, timezone, time window, and DLQ. Limits are in the millions. Console: EventBridge → Scheduler → Schedules.

FeatureRulesScheduler
Cron + rate syntax
Per-schedule timezone
One-time schedules
Flexible time windows
Built-in DLQ✓ (per schedule)
Multiple targets per schedule
Scale ceiling~100/regionmillions

For new work, use Scheduler. It has every feature Rules has plus the per-schedule timezone and DLQ that Rules is missing. The only reason to use Rules today is if you need multiple targets per rule (Scheduler is one-target-per-schedule) or you’re maintaining existing infrastructure that uses Rules.

The rest of this guide focuses on Scheduler with notes about Rules where they differ.

Schedule expressions

Both services accept three expression types:

cron(...) — 6-field with year:

cron(0 12 * * ? *)        # daily at 12:00 UTC
cron(0 9 ? * MON-FRI *)   # weekdays at 09:00 UTC
cron(*/5 * * * ? *)       # every 5 minutes
cron(0 0 L * ? *)         # last day of every month at 00:00

Fields: minute, hour, day-of-month, month, day-of-week, year. Exactly one of day-of-month or day-of-week must be ? — the placeholder meaning “no specific value.” Sunday is 1, Saturday is 7. See the AWS EventBridge dialect reference for the full syntax including L, W, and #.

rate(...) — fixed interval, no clock alignment:

rate(5 minutes)
rate(1 hour)
rate(7 days)

Use rate() when you want “every N units from when this was created” rather than “every N units aligned to clock time.” Minimum granularity is 1 minute.

at(...) — one-time schedule (Scheduler only):

at(2026-12-31T23:59:59)

Useful for scheduled future tasks (a reminder, a delayed action, a planned cutover). The schedule fires once and then can be configured to auto-delete.

Targets and IAM

A target is what gets invoked when the schedule fires. Via its universal target, EventBridge Scheduler can invoke over 6,000 API operations across more than 270 AWS services. The most common:

  • Lambda functionInvoke
  • ECS taskRunTask
  • Step Functions state machineStartExecution
  • SNS topicPublish
  • SQS queueSendMessage
  • EventBridge event busPutEvents

Every schedule needs an IAM role with permission to invoke its target. The role must trust scheduler.amazonaws.com:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "scheduler.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

And it must have permission to call the target operation:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "lambda:InvokeFunction",
    "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function"
  }]
}

Common mistake: granting lambda:InvokeFunction to a Lambda but not granting scheduler.amazonaws.com permission to assume the role. Both sides of the trust relationship have to be set.

Terraform example:

resource "aws_scheduler_schedule" "nightly_report" {
  name = "nightly-report"
  schedule_expression          = "cron(0 0 * * ? *)"
  schedule_expression_timezone = "America/New_York"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_lambda_function.report.arn
    role_arn = aws_iam_role.scheduler_role.arn
  }
}

Timezones

EventBridge Scheduler supports per-schedule timezones via ScheduleExpressionTimezone. Pass any IANA timezone name:

ScheduleExpression: cron(0 9 ? * MON-FRI *)
ScheduleExpressionTimezone: America/Los_Angeles

This runs at 9 AM Pacific time, automatically adjusting for DST.

EventBridge Rules does not support per-rule timezones. Everything is UTC. To run at 9 AM Pacific year-round, you’d need two rules (PST and PDT) gated by date — same workaround as GitHub Actions, equally awkward. If you need timezone-aware Rules, migrate to Scheduler.

Flexible time windows

EventBridge Scheduler supports “flexible time windows” — instead of firing exactly at the scheduled time, fire within a window:

FlexibleTimeWindow: { Mode: FLEXIBLE, MaximumWindowInMinutes: 15 }

With this set, a schedule of cron(0 9 * * ? *) will fire anywhere between 9:00 and 9:15 UTC. AWS chooses the exact time within the window to spread load across customers — useful when you have thousands of similar schedules that would otherwise stampede the same target.

Default mode is OFF (fire exactly at the scheduled time). Set FLEXIBLE for any schedule where ±15 minutes is acceptable and you’d benefit from jitter.

Dead-letter queues

When a target invocation fails (Lambda errored, ECS task wouldn’t start, IAM role lost permission), EventBridge retries with exponential backoff for up to 24 hours. If still failing after that, the event is discarded — silently.

Configure a DLQ on every production schedule:

resource "aws_scheduler_schedule" "nightly_report" {
  # ...
  target {
    arn      = aws_lambda_function.report.arn
    role_arn = aws_iam_role.scheduler_role.arn
    dead_letter_config {
      arn = aws_sqs_queue.scheduler_dlq.arn
    }
  }
}

The DLQ is an SQS queue you create. Failed events are sent there with the original payload + failure metadata. Set up a CloudWatch alarm on the queue’s ApproximateNumberOfMessagesVisible to alert when failures pile up.

For EventBridge Rules, the DLQ is configured per-target in the rule definition. Same SQS-queue pattern.

Monitoring and debugging

CloudWatch metrics emitted by EventBridge Scheduler (namespace AWS/Scheduler):

  • Invocations — count of successful target invocations
  • InvocationAttemptCount — total attempts (includes retries)
  • InvocationsFailedToBeSentToDeadLetterQueue — bad sign, the DLQ itself is failing
  • InvocationDroppedCount — events that exhausted retries and weren’t DLQ’d

Set CloudWatch alarms on:

  • Failures-to-DLQ > 0 (an actual incident)
  • Invocations dropping to 0 when you expect activity (the schedule isn’t firing)

For ad-hoc debugging:

  • Verify the cron expression — paste it into the parser with the EventBridge dialect selected. Check next-run times. Remember to add cron(...) wrapper.
  • Check IAM — the schedule’s role must trust scheduler.amazonaws.com AND have permission to invoke the target.
  • Check the schedule’s recent history — Scheduler shows recent invocation attempts in the console.
  • Test the target manually — invoke the Lambda or ECS task directly to confirm it works outside the scheduler. If it works manually but not via Scheduler, the issue is IAM or input shape.
  • Check the DLQ — failed events with detailed error info are sitting there.

For one-off testing of a not-yet-deployed schedule, use at(...) with a near-future time to fire once and observe.

Frequently asked questions

What's the difference between EventBridge Rules and EventBridge Scheduler?
Rules is the classic service (originally CloudWatch Events) — always UTC, scales to ~100 rules per region per account, simpler API. Scheduler is the newer service (late 2022) — per-schedule timezones, flexible time windows, dead-letter queues, scales to millions of schedules. For new scheduled work, start with Scheduler.
What cron syntax does EventBridge use?
Six fields with year (no seconds): minute, hour, day-of-month, month, day-of-week, year. Sunday is 1, Saturday is 7. Exactly one of day-of-month or day-of-week must be `?`. Expressions are wrapped in `cron(...)`. Example: `cron(0 12 * * ? *)` for daily at noon UTC.
Can I use rate() instead of cron()?
Yes — `rate(5 minutes)`, `rate(1 hour)`, `rate(7 days)` etc. for simple fixed intervals. Minimum is 1 minute. Use rate() when you want "every N units" without alignment to clock time, cron() when alignment matters.
How do I run an EventBridge schedule in a specific timezone?
EventBridge Scheduler supports `ScheduleExpressionTimezone` (IANA timezone name). Classic EventBridge Rules are UTC-only — you have to offset the hour manually.
What happens when an EventBridge target fails?
Without a DLQ configured, the invocation is retried with exponential backoff for up to 24 hours, then discarded. With a DLQ (an SQS queue you provide), failed events are sent to the queue for inspection and replay.