EventBridge Scheduler for standalone schedules, Step Functions Wait states for delays that live inside a workflow, and legacy EventBridge cron rules only for the UTC jobs you already run and have no reason to migrate. Scheduler brought timezones, one-time schedules and per-schedule retries to serverless AWS; the older rule-based cron has none of that. Here is where each option wins, and where naive cron quietly breaks twice a year.
Why do legacy EventBridge cron rules break on daylight saving time?
Cron expressions on EventBridge rules evaluate in UTC only. A report meant for 09:00 in Paris fires at 09:00 in winter and 10:00 in summer, because the UTC hour never moves while local time does. Rules also cannot express one-time schedules, so every "run this once, later" becomes a workaround.
The limitations are structural. Rules live at the event bus level, share a default quota of a few hundred rules per bus, and configure retries per target rather than per schedule. Teams patch the one-off gap by creating a rule, letting it fire, then deleting it from inside the handler: fragile plumbing for something the platform now does natively. Cron rules remain a reasonable choice in exactly one case: a small, stable set of genuinely UTC jobs (nightly exports, log rotation) that is already deployed, already monitored, and not worth touching.
What does EventBridge Scheduler do that cron rules cannot?
EventBridge Scheduler is a separate service built for scheduling: timezone-aware cron with correct DST handling, one-time schedules through at() expressions, flexible time windows to spread load, and account quotas measured in millions of schedules. Each schedule carries its own retry policy and dead-letter queue. In 2026 it is the default choice.
Its most underrated feature is universal targets. A schedule can call hundreds of AWS services directly through their APIs: send an SQS message, start an ECS task, kick off a Step Functions execution, all without a glue Lambda whose only job is to forward the call. Fewer functions means fewer cold starts, fewer IAM roles and less code to patch. When the target is a queue feeding workers, the schedule slots straight into the fan-out design we describe in our EventBridge, SQS and Lambda article, and failed deliveries land in the schedule's dead-letter queue instead of vanishing.
When do Step Functions Wait states beat a scheduler?
When the delay belongs inside a workflow. "Wait three days after signup, then check activity and send an email" is one execution that keeps its context, not three schedules rebuilding state from a database. Wait states pause on a fixed duration or on a timestamp read from the input, and Standard workflows can pause for up to a year.
The same machinery covers human approvals: a task token parks the execution until someone calls back, whether that takes an hour or a month, with no compute billed while it sleeps. The trap is workflow type: Express workflows bill on duration and cap executions at a few minutes, so long waits belong in Standard. If you are weighing a state machine against a chain of Lambdas, we cover that trade-off in our Step Functions orchestration article. Use Wait for delays between steps; use Scheduler to start the workflow in the first place.
Not sure which scheduling primitive fits your workload? Describe your system: a one-page diagnosis within 48 hours.
Get my diagnosis →EventBridge Scheduler vs cron rules vs Wait states: the comparison
The three tools barely overlap once you name the axis. Cron rules and Scheduler both start work on a clock, but only Scheduler understands local time and one-off runs. Wait states start nothing: they pause work already in flight. The table below draws the boundaries, with the winning scenario for each.
| Criterion | EventBridge cron rules | EventBridge Scheduler | Step Functions Wait |
|---|---|---|---|
| Timezone support | UTC only | Any timezone, DST handled | UTC timestamps read from the state input |
| One-off schedules | Not supported | Native at() expressions, self-deleting | Any delay inside a running execution |
| Scale | A few hundred rules per bus (default quota) | Quotas in the millions of schedules | One wait per step; Standard pauses up to a year |
| Target types | Bus targets (Lambda, SQS, SNS, and more) | Hundreds of AWS APIs called directly | Whatever the next state does |
| Retry and DLQ | Per target on the rule | Per schedule, with backoff | Per state, via workflow error handling |
| When it wins | Existing stable UTC jobs with nothing to migrate | Almost everything else: recurring jobs, reminders, one-offs | Delays and approvals inside a workflow that holds context |
How do you build per-user reminders without a polling cron?
Create one one-time schedule per reminder and let it delete itself after firing. This replaces the classic design where a cron Lambda scans a reminders table every minute, pays for mostly empty invocations, and still misses the exact minute under load. With quotas in the millions, one schedule per user is not an anti-pattern: it is the intended usage.
// One schedule per reminder, removed automatically after it fires
await scheduler.send(new CreateScheduleCommand({
Name: `reminder-${reminderId}`,
ScheduleExpression: "at(2026-10-05T09:00:00)",
ScheduleExpressionTimezone: "Europe/Paris",
FlexibleTimeWindow: { Mode: "OFF" },
ActionAfterCompletion: "DELETE",
Target: {
Arn: "arn:aws:sqs:eu-west-1:123456789012:reminders",
RoleArn: "arn:aws:iam::123456789012:role/scheduler-to-sqs",
},
}));
Two details matter in production. Keep the consumer idempotent, because a retry can deliver the same reminder twice. And attach a dead-letter queue to each schedule, so a failed reminder becomes a message you can replay rather than a silent gap in your product.
What do the three options cost?
Shapes, not figures: Scheduler bills per invocation past a monthly free allowance, scheduled rules ride on EventBridge pricing, and Standard Step Functions bill per state transition, so a paused Wait state costs nothing while it sleeps. A polling cron, by contrast, pays for every empty Lambda invocation, all day, every day.
That is the economic argument for the reminder pattern above: polling converts idle time into spend, while per-reminder schedules only pay when something actually happens. On the workflow side, remember that Express workflows bill on duration, which makes them exactly wrong for long waits; Step Functions pricing details the split. Check the current numbers on the official pages before modelling anything: free allowances and tiers move.
Decision checklist
- Recurring job in a local timezone, or any one-off run: EventBridge Scheduler.
- Per-user reminders: one self-deleting schedule each, never a polling cron.
- Delay or approval inside a workflow that holds context: a Wait state, in a Standard workflow.
- Existing UTC cron rules running fine: leave them alone, migrate when you next touch them.
- Everywhere: idempotent consumers, a dead-letter queue, and an alarm on that queue.