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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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's ErrorCode and ErrorMessage.
  • 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.
  • Detail integers 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 resources array, 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 matching iam.amazonaws.com API 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

TargetPermission EventBridge needsWatch out
LambdaFunction 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
SQSQueue policy: events.amazonaws.com, sqs:SendMessage, aws:SourceArn = rule ARNEncrypted queue: a customer managed key allowing events.amazonaws.com kms:Decrypt and kms:GenerateDataKey (not aws/sqs)
SNSTopic policy: events.amazonaws.com, sns:PublishEncrypted 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 actionRegional 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::Permission for events.amazonaws.com with SourceArn set to the rule;
  • a queue policy on the DLQ that allows events.amazonaws.com sqs:SendMessage with aws:SourceArn set to the rule;
  • a target RetryPolicy with MaximumRetryAttempts 8 and MaximumEventAgeInSeconds 7200.

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 example NO_PERMISSIONS, THROTTLING, TIMEOUT), ERROR_MESSAGE, EXHAUSTED_RETRY_CONDITION and RETRY_ATTEMPTS.
  • Alarms:
  • FailedInvocations, Sum ≥ 1 (AWS suggests 10 periods);
  • also InvocationsSentToDLQ and InvocationsFailedToBeSentToDLQ;
  • TriggeredRules versus Invocations shows 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.

Read this document as markdown — the same bytes an agent receives.