EventBridge
After this you can answer: When would you choose EventBridge over SNS, and how would you schedule a reminder that is neither late nor sent twice?
Picture it
Section titled “Picture it”Where it sits
Section titled “Where it sits”Our booking events already flow through SNS to two queues. The reminder would be a third consumer of the same topic. It creates one schedule per booking; Scheduler fires it 24 hours before the event.
flowchart TB topic["minitickets-dev-booking-events SNS"] -->|"copies event - proposed queue policy, sqs:SendMessage"| inq["Reminder-setup SQS (designed, not built)"] inq -->|"polled - proposed mapping, sqs:ReceiveMessage"| setup["Schedule-setup Lambda (designed, not built)"] setup -->|"creates one-time schedule - scheduler:CreateSchedule, iam:PassRole"| sched["EventBridge Scheduler (designed, not built)"] sched -->|"sends job when due - execution role sqs:SendMessage"| outq["Reminder SQS (designed, not built)"] outq -->|"polled - proposed mapping, sqs:ReceiveMessage"| sender["Reminder Lambda (designed, not built)"] sender -->|"sends email - ses:SendEmail with ses:FromAddress condition"| ses["SES identities"] class topic,ses aws class inq,outq aws class inq,outq designed class setup,sender app class setup,sender designed class sched service class sched designedLegend: highlighted = EventBridge Scheduler; app = our code; AWS = managed service; dashed = designed, not built. The topic and SES identities exist in code.
Open the booking-events topic in the map → · Open SES →. No EventBridge piece exists: nothing is deployed or defined.
How it works
Section titled “How it works”First, a plain event bus. This is the mechanism behind every EventBridge interview question.
- A producer calls
PutEventswith up to 10 entries. Each entry has aSource(who sent it, for exampleminitickets.bookings), aDetailType(what happened, for exampleBookingConfirmed) and a JSONDetail(the data). - The bus accepts the entry and returns an event ID. Acceptance is all the producer learns. It does not know which targets, if any, will receive it.
- The bus compares the event with every rule on that bus. An event pattern matches field by field:
{"source": ["minitickets.bookings"], "detail": {"quantity": [{"numeric": [">", 4]}]}}matches large bookings only. One event can match many rules, or none. No match means the event is silently dropped. - For each matching rule, EventBridge sends the event to each of the rule’s targets. It can first reshape the event with an input transformer, which picks fields into a new JSON.
- If a target fails with a retryable error, EventBridge retries with backoff for up to 24 hours and 185 attempts by default. After that, the event goes to the rule target’s dead-letter queue (DLQ) if you set one. Otherwise it is lost.
Now Scheduler, which the reminder uses. It has no bus and no rules: a schedule names its own target.
sequenceDiagram participant S as Schedule-setup Lambda (designed, not built) box rgb(237,233,254) EventBridge Scheduler participant ES as Scheduler (designed, not built) end participant Q as Reminder SQS (designed, not built) participant R as Reminder Lambda (designed, not built) participant E as SES S->>ES: CreateSchedule reminder-bookingId, at(startsAt minus 24 h) ES-->>S: Created (or ConflictException if it exists) Note over ES: Waits, then fires within the chosen minute ES->>Q: SendMessage, execution role Q->>R: Batch via event source mapping R->>R: Skip if already sent or event started R->>E: SendEmail E-->>R: Accepted, message ID R-->>Q: Failed message IDs onlyLegend: arrows show order; every participant except SES is designed, not built. SES accepting a message is not proof of delivery to the inbox.
Under the hood
Section titled “Under the hood”Two bus types. The default bus in every account, and the custom buses most tutorials create, route with rules and targets. AWS now calls a custom bus of that kind Custom Event Bus - Classic. A newer Custom Event Bus works differently: it retains every event for 1 to 365 days, consumers create their own subscribers (a filter plus one target each), and a subscriber can start from a past position, which is how it replays. It also offers ordered delivery within an event group and deduplication at publish time. The two have separate quotas and retry defaults. Everything else on this page describes the default and Classic buses unless it says otherwise. AWS: default and Classic buses, AWS: Custom Event Bus
Acceptance is not delivery. PutEvents can return HTTP 200 while some entries failed: check FailedEntryCount and resend only the entries that have an ErrorCode. There is a sharper trap. If you publish to an event bus that does not exist, AWS documents that EventBridge drops the event, returns 200, and does not count it as failed. Verify bus names in configuration and tests. AWS: PutEvents
Patterns. Matching is exact and typed. "4" (a string) does not match 4 (a number). Field names are case-sensitive. Patterns also support prefix, suffix, numeric ranges, anything-but and exists. A rule whose target publishes events that the same rule matches makes an infinite loop and a large bill. AWS: event patterns
Archive and replay. An archive stores the events of one bus that match an optional pattern, for a retention period you set. A replay sends archived events from a time range back to the same bus, to all rules or to rules you pick. Replay does not rewind a database or unsend an email. Replayed events may arrive in a different order, so consumers need idempotency: handling the same event twice must have the same effect as once. AWS: archive and replay
Scheduler. A schedule has an expression, a time zone, a target and an execution role.
| Expression | Example | Meaning |
|---|---|---|
at() | at(2026-12-24T19:30:00) | Once, at that local time in the schedule’s time zone |
rate() | rate(15 minutes) | Repeat every interval; days means exactly 24 hours |
cron() | cron(0 9 * * ? *) | Calendar times: 09:00 every day in the chosen time zone |
Every schedule fires with 60-second precision: a schedule for 1:00 fires between 1:00:00 and 1:00:59. A flexible time window, for example 15 minutes, lets Scheduler spread many invocations out. With daylight saving, a cron time that does not exist (spring forward) is skipped, and a repeated hour (fall back) runs once. ActionAfterCompletion: DELETE removes a one-time schedule after it runs; completed schedules otherwise stay and count toward the quota. AWS: schedule types
Scheduler has its own retry policy: by default it retries for 24 hours and up to 185 times. Unlike Classic rules, its DLQ is written with the schedule’s execution role, so that role needs sqs:SendMessage on the DLQ, and the DLQ cannot be FIFO. AWS: Scheduler DLQ
Scheduled rules on an event bus still exist, but AWS labels them legacy. Use Scheduler for new work: it adds one-time schedules, time zones and far higher limits. AWS: scheduled rules (legacy)
Limits and numbers
Section titled “Limits and numbers”At the time of writing; check the AWS docs. Read on 2026-10-19 from the EventBridge quotas and Scheduler quotas pages. Nothing here was measured in our account.
| Item | Value | Why it matters |
|---|---|---|
PutEvents entries per request (Classic) | 10; total request under 1 MB | Older material says 256 KB per event; check the current page |
PutEvents rate, us-east-1 (Classic) | 10,000 per second, adjustable | Producers past this get throttled |
| Targets per rule (Classic) | 5, not adjustable | More consumers means more rules |
| Rules per bus (Classic) | 300 in most Regions, adjustable | One rule per consumer and event type adds up |
| Target invocations, us-east-1 (Classic) | 18,750 per second, adjustable; throttled, not dropped | Bursts wait instead of failing |
| Rule target retries (Classic default) | 24 hours, 185 attempts | Set a shorter age when late is useless |
| Schedules per Region | 10,000,000, adjustable; includes completed one-time schedules | Delete after completion |
CreateSchedule rate, us-east-1 | 5,000 per second, adjustable | Fine for one schedule per booking |
| Scheduler invocations, us-east-1 | 1,000 per second, adjustable; throttled, then delayed | Many reminders in one minute arrive late |
| Scheduler target input | 256 KB, fixed | Keep the reminder job small |
| Scheduler precision | 60 seconds without a flexible window | Not a per-second timer |
In MiniTickets
Section titled “In MiniTickets”Explain-only: designed, not built. There is no event bus, rule, archive, schedule or reminder code in this repository.
What exists today: the outbox relay publishes each confirmed booking to minitickets-dev-booking-events (infra/src/jobs/booking-events.ts → createBookingEvents). The email and tickets queues subscribe with raw delivery. The email worker, apps/worker/src/handlers/booking-confirmed.ts → handleBookingConfirmed, builds the email from the message alone and never reads the database. It records each job in the minitickets-dev-processed-jobs DynamoDB table under the key booking.confirmed:<bookingId>.
Why SNS and not EventBridge today: we have one event type and two consumers that want all of it. SNS fan-out to SQS does that with two subscriptions. EventBridge would pay off with many event types, content-based routing, AWS or SaaS event sources, or archive and replay.
The reminder design, step by step:
- A third subscription gives the reminder its own queue copy of each booking event. A slow or broken reminder path cannot delay confirmation emails or tickets.
- The schedule-setup Lambda computes
reminderAt = eventStartsAt − 24 hours.eventStartsAtis already in the message, in UTC. Oureventstable storesstarts_atas a UTC timestamp and no venue time zone. “09:00 venue time on the day before” therefore needs a new column first; “24 hours before” works today. - It names the schedule
reminder-<bookingId>. Names are unique within a schedule group, so a duplicate booking message getsConflictException, which the Lambda treats as success. The name is the idempotency key for creating schedules. - If
reminderAtis already past (the booking was made less than 24 hours before), the Lambda follows a product rule: skip the reminder. It decides this in code; it does not rely on what Scheduler does with a time in the past. - The schedule’s target is the reminder queue, with
ActionAfterCompletion: DELETE. The input carries what the email needs, like the confirmation message does: booking ID, event title, venue, start time and the customer’s email address. That personal data then sits in Scheduler until the schedule fires. The alternative, a database read at send time, would need network access our Lambdas do not have. - The reminder Lambda uses the same processed-jobs pattern with a new key,
booking.reminder:<bookingId>. Reusingbooking.confirmed:would make the reminder look like a duplicate of the confirmation and skip it. It also skips if the event has already started, because a late reminder is worse than none.
MiniTickets has no booking cancellation or event date change today. If they are added, the change must also update or delete reminder-<bookingId>, and the reminder Lambda should check current state before sending.
Connections
Section titled “Connections”| Connection | What allows it | Boundary to keep |
|---|---|---|
| Topic → reminder-setup queue (proposed) | New SNS subscription; queue policy allowing sns.amazonaws.com sqs:SendMessage, aws:SourceArn = our topic | Independent of email and tickets |
| Setup Lambda → Scheduler (proposed) | Its role: scheduler:CreateSchedule on the reminder group, plus iam:PassRole for the Scheduler execution role only | PassRole limited to that one role ARN |
| Scheduler → reminder queue (proposed) | Execution role trusted by scheduler.amazonaws.com (with aws:SourceAccount), allowed sqs:SendMessage on that queue | One queue; also the DLQ if set |
| Reminder Lambda → SES (proposed) | Its role: ses:SendEmail on our identities with ses:FromAddress equal to our sender, like the worker role | Cannot send as anyone else |
| Producer → event bus (general) | events:PutEvents on that bus ARN; a bus resource policy for other accounts | Exact bus |
| Rule → SQS or Lambda target (general) | Queue policy or Lambda resource policy for events.amazonaws.com scoped to the rule ARN, or a target role | Scope to the rule, not the whole service |
iam:PassRole is the easy one to miss. Scheduler acts with the execution role named in the schedule. The caller must be allowed to hand over (“pass”) that role, or CreateSchedule fails with AccessDenied even though the caller has scheduler:CreateSchedule. AWS: Scheduler execution role, AWS: EventBridge target permissions
When it fails
Section titled “When it fails”flowchart TB due["Schedule fires"] --> send{"SendMessage to reminder queue"} send -->|"accepted"| worker["Reminder Lambda handles it"] send -->|"denied or throttled"| retry["Scheduler retries with backoff"] retry -->|"age or attempts used up"| dlq["Scheduler DLQ, or lost if none"] worker -->|"SES error, 3 tries"| wdlq["Reminder queue DLQ"] class due service class due designed class send,worker app class send,worker designed class retry,dlq,wdlq failureLegend: red = failure path; two separate DLQs for two separate boundaries. All parts are designed, not built.
| What you see | Why | Fix |
|---|---|---|
PutEvents returns 200, no target ever runs | Wrong bus name, rule disabled, or pattern does not match (typed values, case) | Test the exact event against the rule with TestEventPattern; check the bus name |
FailedInvocations on the rule metric | Target policy or role does not allow delivery | Check the queue or Lambda resource policy names events.amazonaws.com and the rule ARN |
CreateSchedule AccessDenied with scheduler:CreateSchedule allowed | Caller lacks iam:PassRole for the execution role | Allow PassRole on that one role ARN |
| Schedule fired, queue empty, nothing in DLQ | Execution role trust or sqs:SendMessage missing, and no DLQ configured | Add the DLQ and its permission first, then fix the role |
| Reminder arrives an hour off | Time zone or daylight-saving assumption (UTC versus venue time) | Store the venue time zone; set ScheduleExpressionTimezone |
Once SQS accepts the job, Scheduler is done. An SES failure after that is the reminder queue’s problem: its retries and its DLQ. Watch both. AWS: Scheduler troubleshooting
At 10x / 100x
Section titled “At 10x / 100x”At 10x, many events start at 19:30, so reminders cluster at 19:30 the day before. Scheduler’s default 1,000 invocations per second in us-east-1 throttles and delays the rest, which is fine for an email. A flexible time window of 15 minutes spreads them further. The reminder queue absorbs the burst; the Lambda’s concurrency limit keeps SES within its sending rate.
At 100x, one schedule per booking is still within the defaults: millions of schedules are allowed, and ActionAfterCompletion: DELETE keeps the count from growing forever. The cost to watch is cancellations and date changes: each one is an update or delete call per booking.
The alternative is one schedule per event. At reminder time it enqueues one job per booking, page by page. Fewer schedules, but the fan-out code needs pagination, progress tracking and restart safety, and it needs to read bookings from the database. Pick per-booking schedules while bookings change rarely; pick per-event when there are many bookings per event and frequent changes.
For a bus, more consumers means more rule invocations even if producers stay the same. Measure MatchedEvents, Invocations and FailedInvocations per rule, not just PutEvents calls.
Common interview traps
Section titled “Common interview traps”- “Scheduler needs an event bus.” A schedule calls its target directly. Scheduled rules on a bus are the legacy way.
- “200 from PutEvents means it was delivered.” It means accepted. Entries can fail inside a 200, and a missing bus drops events silently.
- “An event pattern controls who can receive events.” Patterns choose events. IAM roles and resource policies allow delivery.
- “Replay is safe because it is old data.” Replay sends the events again. A non-idempotent consumer sends the email again.
- “rate(1 day) means 9 AM every day.” A rate counts 24-hour intervals. Use
cron()with a time zone for wall-clock times.
Design question
Section titled “Design question”Send each customer a reminder the day before their event: never twice, never after the event starts.
Use one one-time schedule per booking, named after the booking ID, so creation is idempotent. Target an SQS queue rather than the email Lambda directly, so the sender has its own retries and DLQ. The sender keeps a processed-jobs record keyed booking.reminder:<bookingId> and skips events that already started. Choose and document the time rule: 24 hours before in UTC now, venue-local 09:00 once events store a time zone.
flowchart TB booking["Booking event (exists)"] --> setup["Create reminder-bookingId (designed, not built)"] setup -->|"created, or ConflictException if it exists"| sched["One-time schedule (designed, not built)"] sched --> queue["Reminder queue (designed, not built)"] queue --> check{"Already sent or event started?"} check -->|"no"| send["Send via SES, record job"] check -->|"yes"| skip["Skip, report success"] class booking aws class setup,check app class setup,check designed class sched service class sched designed class queue aws class queue designed class send,skip app class send,skip designedLegend: dashed = designed, not built; both outcomes are correct. Idempotency sits at both ends: the schedule name and the processed-jobs key.
Would an event bus help? Not for this alone. If later many services react to bookings (analytics, loyalty, partner webhooks), moving the outbox to an EventBridge bus gives per-consumer rules, content filters and archive and replay. That is a migration with its own cost, not a requirement for reminders.
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”When would you pick EventBridge over SNS?
What happens to events that match no rule?
Why put SQS between Scheduler and the email Lambda?
How is the newer Custom Event Bus different?
What if a booking is made 2 hours before the event?
Remember it
Section titled “Remember it”- Rule = event pattern + up to 5 targets; no match means the event is dropped.
PutEvents200 means accepted; checkFailedEntryCountand the bus name.- Scheduler calls targets directly:
at(),rate(),cron(), 60-second precision. - The caller needs
iam:PassRolefor the Scheduler execution role. - In MiniTickets, reminders would be designed per booking, idempotent by name and by processed-jobs key.
Advanced
Section titled “Advanced”Advanced: Console, read-only CLI, input transformers and Pipes
Console: EventBridge → Buses → Event buses → a bus → Rules; EventBridge → Archives and Replays; EventBridge → Scheduler → Schedules. This project has none to open yet.
aws events list-event-buses --region us-east-1aws events list-rules --event-bus-name default --region us-east-1aws scheduler list-schedules --region us-east-1aws scheduler get-schedule --name "$SCHEDULE_NAME" --region us-east-1 --query '{State:State,Expr:ScheduleExpression,Zone:ScheduleExpressionTimezone,Role:Target.RoleArn}'These only read. The --query keeps the target input out of your terminal: for a reminder it would contain a customer’s email address.
The TestEventPattern API checks one sample event against one pattern without creating anything. It is the fastest way to debug “no target ran”. In the console it is EventBridge → Developer resources → Sandbox.
Input transformer. A rule target can pick fields with JSON paths and place them in a template, so the target receives {"bookingId": "...", "kind": "reminder"} instead of the whole envelope. Keep transformations small; business logic belongs in code with tests.
EventBridge Pipes connect one source (an SQS queue, a DynamoDB stream, a Kinesis stream) to one target, with optional filtering and enrichment. A pipe is point to point; a bus is many to many.
API destinations let a rule call an outside HTTPS endpoint with a stored connection (API key, OAuth or basic auth) and a rate limit, for example a partner webhook.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- What does an HTTP 200 from
PutEventsprove, and what does it not? - Why does
CreateScheduleneediam:PassRole? - Which two DLQs does the reminder design have, and which failures land in each?
- How does naming the schedule
reminder-<bookingId>stop duplicate reminders? - Why can “09:00 venue time” not be scheduled from today’s events table?
Redraw it from memory: the booking topic, reminder-setup queue and Lambda, Scheduler, reminder queue and Lambda, and SES, with the permission on each arrow
Try this: open apps/worker/src/handlers/booking-confirmed.ts, find the jobKey line and write the key a reminder would use, and why it must differ
Read this book online: https://book.theversehub.in/