Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
cdk.out
*.js
!src/**/*.js
*.d.ts
package-lock.json
102 changes: 102 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# EventBridge to SQS Fair Queue with Lambda Consumer

This pattern deploys an EventBridge rule that routes events to an SQS fair queue, consumed by a Lambda function. Fair queues ensure equitable processing across tenants — no single tenant can monopolize the consumer.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-sqs-fair-queue-lambda-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

> **⚠️ Important Note:** As of April 2026, `FairQueueConfiguration` is **API-only** and not yet supported in the CloudFormation schema. This means the fair queue configuration cannot be deployed via CDK/CloudFormation. This pattern deploys the standard SQS queue, EventBridge rule, Lambda consumer, and DLQ via CDK, but the fair queue enablement must be done separately via the AWS CLI or SDK after deployment:
>
> ```bash
> aws sqs set-queue-attributes \
> --queue-url <QueueUrl> \
> --attributes '{"FairQueueConfiguration":"{\"MessageGroupIdFieldPath\":\"$.detail.tenantId\"}"}'
> ```
>
> Once CloudFormation adds `FairQueueConfiguration` support, this pattern will be updated to deploy fully via CDK.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js 18+](https://nodejs.org/en/download/) installed
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed

## Architecture

```
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ EventBridge │────▶│ EventBridge │────▶│ SQS Fair Queue │────▶│ Lambda │
│ PutEvents │ │ Rule │ │ (per-tenant │ │ Consumer │
│ (tenant-id) │ │ (source match) │ │ fair sharing) │ │ │
└──────────────┘ └──────────────────┘ └──────────────────┘ └──────────────┘
```

## How it works

1. Events are published to EventBridge with a `detail.tenantId` field identifying the tenant.
2. An EventBridge rule matches events with `source: "com.myapp.orders"` and routes them to an SQS fair queue.
3. The fair queue uses `tenantId` as the message group ID, ensuring equitable consumption across tenants.
4. A Lambda function processes messages from the fair queue. Even if Tenant A sends 1000 messages and Tenant B sends 10, both tenants get fair processing time.
5. A dead-letter queue captures failed messages after 3 retries.

## Deployment Instructions

1. Clone the repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/eventbridge-sqs-fair-queue-lambda-cdk
```

2. Install dependencies:
```bash
npm install
```

3. Deploy the stack:
```bash
cdk deploy
```

4. Note the EventBridge bus name and queue URL from the stack outputs.

## Testing

1. Send events for multiple tenants:
```bash
# Tenant A — burst of events
for i in $(seq 1 5); do
aws events put-events --entries '[{
"Source": "com.myapp.orders",
"DetailType": "OrderCreated",
"Detail": "{\"tenantId\": \"tenant-a\", \"orderId\": \"order-a-'$i'\", \"amount\": 99.99}",
"EventBusName": "<EventBusName>"
}]'
done

# Tenant B — single event
aws events put-events --entries '[{
"Source": "com.myapp.orders",
"DetailType": "OrderCreated",
"Detail": "{\"tenantId\": \"tenant-b\", \"orderId\": \"order-b-1\", \"amount\": 49.99}",
"EventBusName": "<EventBusName>"
}]'
```

2. Check Lambda logs to verify fair processing — Tenant B's message should not be starved by Tenant A's burst:
```bash
aws logs tail /aws/lambda/<FunctionName> --follow
```

## Cleanup

```bash
cdk destroy
```

----
Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
10 changes: 10 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { EventBridgeSqsFairQueueLambdaStack } from "../lib/eventbridge-sqs-fair-queue-lambda-stack";

const app = new cdk.App();
new EventBridgeSqsFairQueueLambdaStack(
app,
"EventBridgeSqsFairQueueLambdaStack"
);
11 changes: 11 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "src"]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true
}
}
61 changes: 61 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"title": "EventBridge to SQS Fair Queue with Lambda Consumer",
"description": "Route events from Amazon EventBridge to an SQS fair queue for equitable multi-tenant processing with Lambda.",
"language": "TypeScript",
"level": "300",
"framework": "AWS CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern deploys an EventBridge rule that routes events to an SQS fair queue, consumed by a Lambda function. Fair queues ensure equitable processing across tenants by preventing any single tenant from monopolizing the consumer.",
"Events are published to EventBridge with a tenant ID. The EventBridge rule matches events and forwards them to an SQS fair queue with the tenant ID as the message group. SQS fair queues automatically balance consumption across groups, ensuring no single tenant starves others.",
"This pattern is ideal for multi-tenant SaaS applications where you need fair processing guarantees without building custom throttling logic."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-sqs-fair-queue-lambda-cdk",
"templateURL": "serverless-patterns/eventbridge-sqs-fair-queue-lambda-cdk",
"projectFolder": "eventbridge-sqs-fair-queue-lambda-cdk",
"templateFile": "lib/eventbridge-sqs-fair-queue-lambda-stack.ts"
}
},
"resources": {
"bullets": [
{
"text": "Amazon EventBridge targets SQS fair queues",
"link": "https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-eventbridge-sqs-fair-queue-targets/"
},
{
"text": "Amazon SQS fair queues",
"link": "https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/fair-queues.html"
},
{
"text": "Amazon EventBridge",
"link": "https://aws.amazon.com/eventbridge/"
}
]
},
"deploy": {
"text": [
"cdk deploy"
]
},
"testing": {
"text": [
"See the GitHub repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk destroy</code>."
]
},
"authors": [
{
"name": "Nithin Chandran R",
"bio": "Technical Account Manager at AWS",
"linkedin": "nithin-chandran-r"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as cdk from "aws-cdk-lib";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as events from "aws-cdk-lib/aws-events";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";

export class EventBridgeSqsFairQueueLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// Dead-letter queue for failed messages
const dlq = new sqs.Queue(this, "DLQ", {
queueName: "fair-queue-dlq",
retentionPeriod: cdk.Duration.days(14),
});

// SQS Fair Queue (uses CfnQueue — fair queues are a new queue type)
const cfnFairQueue = new sqs.CfnQueue(this, "FairQueue", {
queueName: "tenant-fair-queue",
visibilityTimeout: 60,
messageRetentionPeriod: 345600, // 4 days
redrivePolicy: {
deadLetterTargetArn: dlq.queueArn,
maxReceiveCount: 3,
},
});

// Enable fair queue configuration via property override
(cfnFairQueue as any).addPropertyOverride("FairQueueConfiguration", {
NumberOfMessageGroups: 100,
});

const fairQueueArn = cfnFairQueue.attrArn;
const fairQueueUrl = cfnFairQueue.ref;

// Import the fair queue as an IQueue for event source mapping
const fairQueue = sqs.Queue.fromQueueAttributes(this, "FairQueueImport", {
queueArn: fairQueueArn,
queueUrl: fairQueueUrl,
});

// Custom EventBridge bus
const bus = new events.EventBus(this, "OrdersBus", {
eventBusName: "orders-bus",
});

// EventBridge rule — match order events and route to fair queue
// Uses CfnRule because L2 SqsQueue target rejects messageGroupId on non-FIFO queues
const rule = new events.CfnRule(this, "OrderRule", {
eventBusName: bus.eventBusName,
eventPattern: {
source: ["com.myapp.orders"],
},
targets: [
{
id: "FairQueueTarget",
arn: fairQueueArn,
sqsParameters: {
messageGroupId: "$.detail.tenantId",
},
},
],
});

// Grant EventBridge permission to send to the fair queue
const sendPolicy = new iam.PolicyStatement({
actions: ["sqs:SendMessage"],
resources: [fairQueueArn],
principals: [new iam.ServicePrincipal("events.amazonaws.com")],
});
const queuePolicy = new sqs.CfnQueuePolicy(this, "FairQueuePolicy", {
queues: [fairQueueUrl],
policyDocument: {
Statement: [
{
Effect: "Allow",
Principal: { Service: "events.amazonaws.com" },
Action: "sqs:SendMessage",
Resource: fairQueueArn,
Condition: {
ArnEquals: { "aws:SourceArn": rule.attrArn },
},
},
],
},
});

// Lambda consumer
const fn = new lambda.Function(this, "ConsumerFn", {
runtime: lambda.Runtime.NODEJS_20_X,
handler: "index.handler",
code: lambda.Code.fromAsset("src"),
timeout: cdk.Duration.seconds(30),
memorySize: 128,
logRetention: logs.RetentionDays.ONE_WEEK,
});

// SQS event source mapping
fn.addEventSource(
new (require("aws-cdk-lib/aws-lambda-event-sources").SqsEventSource)(
fairQueue,
{ batchSize: 10, maxBatchingWindow: cdk.Duration.seconds(5) }
)
);

new cdk.CfnOutput(this, "EventBusName", { value: bus.eventBusName });
new cdk.CfnOutput(this, "FairQueueUrl", { value: fairQueueUrl });
new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName });
new cdk.CfnOutput(this, "DLQUrl", { value: dlq.queueUrl });
}
}
20 changes: 20 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "eventbridge-sqs-fair-queue-lambda-cdk",
"version": "1.0.0",
"bin": {
"app": "bin/app.ts"
},
"scripts": {
"build": "tsc",
"cdk": "cdk"
},
"dependencies": {
"aws-cdk-lib": "2.180.0",
"constructs": "10.4.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"ts-node": "^10.9.0",
"typescript": "~5.7.0"
}
}
19 changes: 19 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
exports.handler = async (event) => {
for (const record of event.Records) {
const body = JSON.parse(record.body);
const detail = body.detail || {};

console.log(
JSON.stringify({
tenantId: detail.tenantId,
orderId: detail.orderId,
amount: detail.amount,
messageGroupId: record.attributes?.MessageGroupId,
messageId: record.messageId,
timestamp: new Date().toISOString(),
})
);
}

return { batchItemFailures: [] };
};
20 changes: 20 additions & 0 deletions eventbridge-sqs-fair-queue-lambda-cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["es2020"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "build",
"rootDir": ".",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": ["node_modules", "build", "src"]
}