Send Events to EventBridge With AWS SDK v3 (PutEvents)

Several railway tracks converging and splitting at a junction seen from above

Photo by Sergej ***** on Unsplash

To send events with EventBridge PutEvents and AWS SDK v3, create an EventBridgeClient and send a PutEventsCommand with up to 10 Entries, each with Source, DetailType, a JSON-string Detail and an optional EventBusName. The request must stay under 1 MB, and a 200 response can still contain failed entries, so always check FailedEntryCount.

This guide is for Node.js and TypeScript developers who publish application events, such as an order created or a payment failed, to Amazon EventBridge so rules can route them to Lambda functions, queues and other targets. You’ll end up with a typed module that covers the EventBridge PutEvents AWS SDK v3 calls a service needs: batching by count and size, retrying only what’s retryable, and testing a rule’s pattern before you deploy it.

If you’re deciding between EventBridge and SNS, or already publish to SNS, read this next to the guide to publish an SNS message with AWS SDK v3 in TypeScript; the comparison below shows where each fits.

EventBridge or SNS: when should you use PutEvents?

EventBridge PutEvents SNS Publish
Routing Rules match event patterns on any field, including inside detail Subscription filter policies on message attributes or the body
Batch size 10 entries per request 10 messages per PublishBatch
Fan-out Up to 5 targets per rule, many rules per bus Every subscription on the topic
Price (us-east-1, Sept 2026) $1.00 per million custom events, billed in 64 KB chunks $0.50 per million requests after the first million

Choose EventBridge when consumers care about different slices of the same event stream and you want routing in rules rather than in the publisher. Choose SNS when you need cheap, fast fan-out to known subscribers. This guide covers @aws-sdk/client-eventbridge and buses that route with rules, which AWS’s quota documentation now calls Custom Event Bus – Classic. The newer Custom Event Bus with subscribers has its own client (@aws-sdk/client-eventbridgev2) and different limits, including up to 100 entries per request. For high-volume, ordered streams that consumers replay, such as clickstream or telemetry, write to Kinesis Data Streams with PutRecords in AWS SDK v3 instead.

Prerequisites

  • Node.js 18 or later, TypeScript and tsx, with "type": "module" in package.json for the top-level await in the runner.
  • @aws-sdk/client-eventbridge.
  • An event bus: the account’s default bus, or a custom bus created in the console or your infrastructure code, in the same Region as the client.
  • Credentials the SDK can resolve; the guide to the AWS SDK v3 credentials provider chain explains the order it checks.

What goes into a PutEvents entry?

Field Required Notes
Source Yes Who emitted it, such as com.example.orders. Rules usually match on it first.
DetailType Yes Free-form, up to 128 characters, such as OrderCreated.
Detail Yes A string holding a valid JSON object. No other schema is imposed.
EventBusName No Name or ARN; the default bus if omitted.
Resources No ARNs the event is mainly about.
Time No An RFC 3339 timestamp; the SDK takes a Date. The PutEvents call time if omitted.
TraceHeader No An X-Ray trace header, to follow the event through targets.

The three required fields are enforced per entry. If an entry lacks one, EventBridge fails that entry; if no entry in the request has all three, the whole request fails.

How to use EventBridge PutEvents in AWS SDK v3, step by step

  1. Create one client per processAn EventBridgeClient at module level, in the bus’s Region.
  2. Serialize the detailJSON.stringify an object. Invalid JSON fails the entry with MalformedDetail.
  3. Batch by count and by sizeAt most 10 entries, and the sum of entry sizes under 1,048,576 bytes.
  4. Check every responseFailedEntryCount and the per-entry ErrorCode, which line up with your request by index.
  5. Retry only retryable failuresInternalFailure and ThrottlingException, with backoff. Log the rest.
  6. Test the rule’s patternTestEventPatternCommand tells you whether a sample event matches, without deploying a rule.

Example: an order-events publisher

eventbridge-events.ts

// eventbridge-events.ts
// Sending custom events to Amazon EventBridge with AWS SDK for JavaScript v3:
// batches of up to 10 entries under 1 MB, partial-failure handling with retry, and pattern tests.
import { setTimeout as sleep } from "node:timers/promises";
import {
  EventBridgeClient,
  PutEventsCommand,
  TestEventPatternCommand,
  type PutEventsRequestEntry,
} from "@aws-sdk/client-eventbridge";

export const eventBridge = new EventBridgeClient({}); // region from AWS_REGION or your profile

export interface AppEvent {
  source: string; // e.g. "com.example.orders"
  detailType: string; // up to 128 characters, e.g. "OrderCreated"
  detail: Record<string, unknown>; // must serialize to a JSON object
  resources?: string[];
}

const MAX_ENTRIES = 10;
const MAX_BYTES = 1_048_576; // the whole request must be smaller than this
const RETRYABLE = new Set(["InternalFailure", "ThrottlingException"]);
const utf8 = (s: string | undefined): number => (s ? Buffer.byteLength(s, "utf8") : 0);

// Entry size as EventBridge calculates it: Time counts 14 bytes, strings count their UTF-8 bytes.
export function entrySize(e: PutEventsRequestEntry): number {
  return (e.Time ? 14 : 0) + utf8(e.Source) + utf8(e.DetailType) + utf8(e.Detail) +
    (e.Resources ?? []).reduce((sum, r) => sum + utf8(r), 0);
}

function toEntry(e: AppEvent, eventBusName: string): PutEventsRequestEntry {
  return {
    EventBusName: eventBusName,
    Source: e.source,
    DetailType: e.detailType,
    Detail: JSON.stringify(e.detail),
    Resources: e.resources,
    Time: new Date(),
  };
}

// Split into requests of at most 10 entries and less than 1 MB each.
function batches(entries: PutEventsRequestEntry[]): PutEventsRequestEntry[][] {
  const out: PutEventsRequestEntry[][] = [];
  let current: PutEventsRequestEntry[] = [];
  let bytes = 0;
  for (const entry of entries) {
    const size = entrySize(entry);
    if (size >= MAX_BYTES) throw new Error(`Event of ${size} bytes is too large: store the payload in S3 and send a reference`);
    if (current.length === MAX_ENTRIES || bytes + size >= MAX_BYTES) {
      out.push(current);
      current = [];
      bytes = 0;
    }
    current.push(entry);
    bytes += size;
  }
  if (current.length) out.push(current);
  return out;
}

export interface FailedEvent { entry: PutEventsRequestEntry; code: string; message: string }

/** Sends events to one bus. Retries entries that failed with a retryable code; returns the rest. */
export async function putEvents(events: AppEvent[], eventBusName = "default", maxAttempts = 4): Promise<FailedEvent[]> {
  const failed: FailedEvent[] = [];
  for (const batch of batches(events.map((e) => toEntry(e, eventBusName)))) {
    let pending = batch;
    for (let attempt = 1; pending.length && attempt <= maxAttempts; attempt++) {
      const res = await eventBridge.send(new PutEventsCommand({ Entries: pending }));
      if (!res.FailedEntryCount) break;
      const retry: PutEventsRequestEntry[] = [];
      // Result entries are in the same order as the request entries.
      (res.Entries ?? []).forEach((result, i) => {
        const entry = pending[i];
        if (!entry || !result.ErrorCode) return;
        if (RETRYABLE.has(result.ErrorCode) && attempt < maxAttempts) retry.push(entry);
        else failed.push({ entry, code: result.ErrorCode, message: result.ErrorMessage ?? "" });
      });
      pending = retry;
      if (pending.length) await sleep(2 ** attempt * 100 + Math.random() * 100); // backoff with jitter
    }
  }
  return failed;
}

/** Checks a rule's event pattern against a sample event without creating a rule. */
export async function matches(pattern: object, event: AppEvent, account: string, region: string): Promise<boolean> {
  const sample = {
    id: "00000000-0000-0000-0000-000000000000",
    account,
    source: event.source,
    time: new Date().toISOString(),
    region,
    resources: event.resources ?? [],
    "detail-type": event.detailType,
    detail: event.detail,
  };
  const res = await eventBridge.send(new TestEventPatternCommand({
    EventPattern: JSON.stringify(pattern),
    Event: JSON.stringify(sample),
  }));
  return res.Result === true;
}

Calling it from a script:

run-events.ts

// run-events.ts
// Usage: AWS_REGION=us-east-1 EVENT_BUS=orders npx tsx run-events.ts
import { matches, putEvents, type AppEvent } from "./eventbridge-events.js";

const bus = process.env.EVENT_BUS ?? "default";
const region = process.env.AWS_REGION ?? "us-east-1";

const events: AppEvent[] = Array.from({ length: 23 }, (_, n) => ({
  source: "com.example.orders",
  detailType: n % 5 === 0 ? "OrderCancelled" : "OrderCreated",
  detail: { orderId: `o-${2000 + n}`, customerId: "c-42", total: 20 + n * 7 },
}));

// The pattern a rule on this bus would use: large new orders only.
const pattern = {
  source: ["com.example.orders"],
  "detail-type": ["OrderCreated"],
  detail: { total: [{ numeric: [">=", 100] }] },
};
const first = events[21];
if (first) console.log(`pattern matches o-2021: ${await matches(pattern, first, "123456789012", region)}`);

const failed = await putEvents(events, bus);
console.log(`${events.length - failed.length} sent to ${bus}, ${failed.length} failed`);
for (const f of failed) console.log(f.code, f.message, f.entry.Detail);
Terminal

npm install @aws-sdk/client-eventbridge
npm install --save-dev tsx typescript @types/node
npm pkg set type=module

AWS_PROFILE=dev AWS_REGION=us-east-1 EVENT_BUS=orders npx tsx run-events.ts
# pattern matches o-2021: true
# 23 sent to orders, 0 failed

23 events become three requests of 10, 10 and 3 entries. Event o-2021 is an OrderCreated with a total of 167, so it matches the pattern; o-2020 would not, because it’s an OrderCancelled.

How do you handle partial failures and FailedEntryCount?

PutEvents doesn’t fail as a whole when one entry fails. EventBridge keeps processing the rest, and the response Entries array has one result per request entry, in the same order: an EventId for successes, ErrorCode and ErrorMessage for failures. The SDK documents which codes are worth retrying:

  • Retryable: InternalFailure and ThrottlingException.
  • Not retryable: AccessDeniedException, InvalidAccountIdException, InvalidArgument, MalformedDetail, RedactionFailure, NotAuthorizedForSourceException and NotAuthorizedForDetailTypeException.

The SDK’s own retry strategy retries a request that fails outright with a throttling error, but it can’t see failures inside a 200 response. That’s why putEvents keeps its own loop, with exponential backoff and jitter, over only the failed entries. The guide to configure retry and timeout settings in AWS SDK for JavaScript v3 covers the request-level side.

Watch out: sending to an event bus that doesn’t exist returns 200, doesn’t count the event in FailedEntryCount, and the event is dropped because no rule matches it. A typo in EVENT_BUS looks like success.

How big can an EventBridge event be?

The whole request must be smaller than 1 MB (1,048,576 bytes), and a single event can use all of it if it’s the only entry. EventBridge measures an entry as 14 bytes for Time if you set it, plus the UTF-8 bytes of Source, DetailType, Detail and each Resources entry, which is what entrySize implements. The event delivered to targets is larger than the entry because of the envelope around it. For anything close to the limit, AWS recommends storing the payload in S3 and sending a reference. Billing is per 64 KB chunk, so a 100 KB event counts as 2 events. A publisher stuck in a retry loop can multiply that quickly, which is one more reason to create an AWS budget alert with AWS SDK v3 on the account.

Rules, event patterns and TestEventPattern

Targets never see your entry as sent. EventBridge wraps it in an envelope with version, id, detail-type, source, account, time, region, resources and detail, and rules match against that shape. This pattern selects large new orders:

large-orders-pattern.json

{
  "source": ["com.example.orders"],
  "detail-type": ["OrderCreated"],
  "detail": { "total": [{ "numeric": [">=", 100] }] }
}

TestEventPatternCommand needs a full envelope with id, account, source, time, region, resources and detail-type, which is why matches builds one. Put a test like that in CI for every rule, since a pattern that matches nothing fails silently. Numeric matching compares values as 64-bit floats, so integers are exact only up to 253.

From the rule, targets take over. A Lambda target receives the envelope as its event, and the example to invoke a Lambda function with AWS SDK v3 in TypeScript helps when you want to call the same function directly while debugging. An SQS target gives you a buffer; read it as shown in send and receive SQS messages with AWS SDK v3, and give it a redrive policy, which the script to find SQS queues without a dead-letter queue checks. When a Lambda target fails, the guide to investigate Lambda errors with CloudWatch is the next step. A Step Functions state machine can be a rule target too; to start the same workflow from code, see start a Step Functions execution with AWS SDK v3. A Lambda target is also the usual place to send a confirmation email, as shown in the guide to send email with Amazon SES and AWS SDK v3 in TypeScript.

EventBridge’s envelope is its own format, not CloudEvents. If your organization standardizes on CloudEvents, carry that object inside detail and match on its fields there.

Which IAM permissions does PutEvents need?

events:PutEvents is scoped to the event bus ARN. It supports the events:source and events:detail-type condition keys, so a service can be limited to the events it owns; entries that break the condition fail with NotAuthorizedForSourceException or NotAuthorizedForDetailTypeException. events:TestEventPattern has no resource type, and it’s only needed where you run pattern tests.

eventbridge-put-events-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PutOrderEvents",
      "Effect": "Allow",
      "Action": "events:PutEvents",
      "Resource": "arn:aws:events:us-east-1:123456789012:event-bus/orders",
      "Condition": {
        "StringEquals": { "events:source": "com.example.orders" }
      }
    },
    {
      "Sid": "TestPatternsInCi",
      "Effect": "Allow",
      "Action": "events:TestEventPattern",
      "Resource": "*"
    }
  ]
}

To derive the actions from your own module, see how to find the IAM actions your AWS SDK for JavaScript code needs, or paste it into the IAM policy generator for TypeScript code.

Troubleshooting and limits

  • AccessDeniedException for the whole request. The caller lacks events:PutEvents on that bus. The steps to troubleshoot AWS IAM access denied errors apply.
  • Entries fail with NotAuthorizedForSourceException. A policy condition restricts events:source or events:detail-type, and this entry doesn’t match it.
  • MalformedDetail. Detail isn’t valid JSON. Build it with JSON.stringify, never by string concatenation.
  • Events accepted, nothing happens. Wrong bus name or Region, no rule whose pattern matches, or a target that lacks permission to be invoked. Run the pattern through TestEventPattern first.
  • Batch limits. 1 to 10 entries per request, under 1 MB in total, and JSON nested at most 1,000 levels deep.
  • Throughput. The default PutEvents quota in us-east-1 is 10,000 requests per second (adjustable; lower in many Regions). By default you get 300 rules per bus in most Regions and event patterns up to 2,048 characters, both adjustable, and a fixed 5 targets per rule.

Moving a v2 publisher? The AWS SDK JavaScript v2 to v3 converter drafts the change, and the guide to migrate a Node.js app from AWS SDK v2 to v3 covers the rest of the app. To see which rules exist on a bus before you change an event’s shape, ChatWithCloud can answer “Which rules are on the orders event bus, and what are their targets?” from a read-only profile; it runs the AWS calls on your machine and sends the results to the AI model to write the answer.

Frequently asked questions

How many events can I send in one PutEvents call?

Up to 10 entries, as long as the total entry size is under 1 MB. Split larger batches into several requests, as putEvents does.

Does PutEvents throw if one event fails?

No. It returns 200 with FailedEntryCount above zero and an ErrorCode on each failed entry. Only request-level problems, such as missing permission or throttling of the whole call, throw.

How do I send an event to a custom event bus?

Set EventBusName on each entry to the bus name or ARN. Without it, events go to the default bus, where your custom bus’s rules never see them.

How do I test an EventBridge rule pattern without sending events?

Call TestEventPatternCommand with the pattern and a sample event that has id, account, source, time, region, resources and detail-type. It returns Result: true on a match.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud