Route events with EventBridge rules
Send events to an EventBridge bus and route them to Lambda, SQS or SNS reliably — PutEvents in TypeScript with FailedEntryCount handling (10 entries, 1 MB since January 2026), writing and testing event patterns with TestEventPattern, giving each target type its permission (resource policies vs a role), AWS service events and the us-east-1 rule for global services, retries (24 hours, 185 attempts) and a target DLQ, and alarming on FailedInvocations.
markdownguidev4PVR3-DRQBstage: buildtrust: officialservice: eventbridgeinterface: sdkinterface: iacinterface: cliEventBridge PutEventsFailedEntryCountPutEvents partial failureevent bus 1 MB payloadEventBridge ruleevent patternTestEventPatternEventBridge Lambda target permissionadd-permission events.amazonaws.comEventBridge SQS target queue policyEventBridge SNS target topic policyencrypted SQS EventBridge KMSEventBridge retry 24 hours 185 attemptsEventBridge DLQInvocationsSentToDLQFailedInvocations alarmIAM events us-east-1CloudTrail events EventBridgeschedule rule UTCinput transformerCDK events Rule
Most "EventBridge didn't work" reports come down to three things: the pattern didn't match exactly, the target never granted EventBridge permission, or the rule lives in the wrong Region. Test the pattern against a real event, grant the target, and give every important target a DLQ.
1. Publish (custom events)
import { EventBridgeClient, PutEventsCommand, type PutEventsRequestEntry } from "@aws-sdk/client-eventbridge";
const eb = new EventBridgeClient({});
export async function publish(busName: string, detailType: string, details: object[]) {
for (let i = 0; i < details.length; i += 10) { // max 10 entries per call
let entries: PutEventsRequestEntry[] = details.slice(i, i + 10).map((d) => ({
EventBusName: busName,
Source: "com.example.orders", // your own source; "aws." is for AWS services
DetailType: detailType,
Detail: JSON.stringify(d), // must be a JSON object string
}));
for (let attempt = 0; entries.length && attempt < 3; attempt++) {
const res = await eb.send(new PutEventsCommand({ Entries: entries }));
if (!res.FailedEntryCount) { entries = []; break; }
// Results are index-aligned with the request: keep only the failed ones.
const failed = entries.filter((_, idx) => res.Entries?.[idx]?.ErrorCode);
for (const [idx, r] of (res.Entries ?? []).entries())
if (r.ErrorCode) console.warn("PutEvents entry failed", idx, r.ErrorCode, r.ErrorMessage);
entries = failed;
}
if (entries.length) throw new Error(`${entries.length} events not accepted`);
}
}
- HTTP 200 doesn't mean every event was accepted. Check
FailedEntryCount, then each entry'sErrorCodeandErrorMessage. - Size: the whole request must be under 1 MB. Event buses accept 1 MB payloads since 2026-01-29, except in Asia Pacific (New Zealand, Thailand, Malaysia, Taipei) and Mexico (Central). Whether every target accepts 1 MB isn't stated, so for large data put it in S3 and send the key.
Detailintegers must fit in a signed 64-bit integer, and nesting is limited to 1,000 levels.
2. Write and test the pattern
import { EventBridgeClient, TestEventPatternCommand } from "@aws-sdk/client-eventbridge";
const eb = new EventBridgeClient({});
const pattern = { source: ["com.example.orders"], "detail-type": ["OrderPlaced"], detail: { total: [{ numeric: [">", 100] }] } };
const sample = {
id: "1", "detail-type": "OrderPlaced", source: "com.example.orders", account: "111111111111",
time: "2026-10-18T12:00:00Z", region: "us-east-1", resources: [], detail: { total: 250 },
};
const { Result } = await eb.send(new TestEventPatternCommand({ EventPattern: JSON.stringify(pattern), Event: JSON.stringify(sample) }));
console.log("matches:", Result);
- Use a real event copied from logs or the console's sandbox as the sample.
- Matching is exact and case-sensitive: an ARN with
:doesn't match one with/. - CloudTrail-based "AWS API Call" events have no
resourcesarray, so don't match on it. - Pattern syntax and limits:
reference/eventbridge/event-patterns.md.
3. Create the rule in the right Region
- Rules see events on their own bus in their own Region.
- Events from global services such as IAM and Route 53 are emitted only in
us-east-1, so a rule matchingiam.amazonaws.comAPI calls must live there. - Schedule rules run in UTC, within a minute of the scheduled time; cron resolution is one minute.
4. Give each target its permission
| Target | Permission EventBridge needs | Watch out |
|---|---|---|
| Lambda | Function resource policy: aws lambda add-permission --function-name <fn> --statement-id <id> --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn <rule-arn> | Don't add SourceAccount: it stops invocation. For an alias or version, add --qualifier |
| SQS | Queue policy: events.amazonaws.com, sqs:SendMessage, aws:SourceArn = rule ARN | Encrypted queue: a customer managed key allowing events.amazonaws.com kms:Decrypt and kms:GenerateDataKey (not aws/sqs) |
| SNS | Topic policy: events.amazonaws.com, sns:Publish | Encrypted topic: the same key statement |
| Role-based targets (Step Functions, Kinesis, another bus, …) | An IAM role that trusts events.amazonaws.com and allows the target action | Regional STS endpoints must be enabled |
The console adds these permissions for you; PutTargets through the API, CLI or SDK does not. The CDK adds them when you use its target classes:
import { Duration, Stack } from "aws-cdk-lib";
import type { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as sqs from "aws-cdk-lib/aws-sqs";
import type * as lambda from "aws-cdk-lib/aws-lambda";
export function orderRule(scope: Construct, fn: lambda.IFunction) {
const stack = Stack.of(scope);
const bus = new events.EventBus(stack, "OrdersBus");
const dlq = new sqs.Queue(stack, "OrdersRuleDlq", { retentionPeriod: Duration.days(14) });
new events.Rule(stack, "BigOrders", {
eventBus: bus,
eventPattern: { source: ["com.example.orders"], detailType: ["OrderPlaced"] },
targets: [new targets.LambdaFunction(fn, { deadLetterQueue: dlq, retryAttempts: 8, maxEventAge: Duration.hours(2) })],
});
return bus;
}
Synthesizing it produced:
- an
AWS::Lambda::Permissionforevents.amazonaws.comwithSourceArnset to the rule; - a queue policy on the DLQ that allows
events.amazonaws.comsqs:SendMessagewithaws:SourceArnset to the rule; - a target
RetryPolicywithMaximumRetryAttempts8 andMaximumEventAgeInSeconds7200.
5. Retries, DLQ and monitoring
- Default: EventBridge retries for up to 24 hours and 185 attempts, with exponential backoff and jitter, and then drops the event. Narrow this per target with
RetryPolicy(MaximumRetryAttempts,MaximumEventAgeInSeconds). - Not retried:
- missing permissions, a deleted target, or a bad address go straight to the DLQ if there is one;
- a throttling target may not be retried either.
- DLQ: must be a standard SQS queue in the same Region, with a queue policy for
events.amazonaws.com(aws:SourceArn= rule ARN). - Each DLQ message carries the attributes
RULE_ARN,TARGET_ARN,ERROR_CODE(for exampleNO_PERMISSIONS,THROTTLING,TIMEOUT),ERROR_MESSAGE,EXHAUSTED_RETRY_CONDITIONandRETRY_ATTEMPTS. - Alarms:
FailedInvocations, Sum ≥ 1 (AWS suggests 10 periods);- also
InvocationsSentToDLQandInvocationsFailedToBeSentToDLQ; TriggeredRulesversusInvocationsshows match-but-no-delivery.- Rarely, a target receives an event more than once, so make targets idempotent.
- Loops: a rule whose target changes the resource it watches can trigger itself. Match only the bad state.
Errors and the full troubleshooting table: reference/eventbridge/errors.md.