Example project: SQS worker on Lambda with DLQ and alarms (CDK, TypeScript)
A complete CDK v2 TypeScript project that builds — an SQS queue with a DLQ, a Lambda worker with partial batch responses and JSON logs, a log group with retention, and error-rate and DLQ alarms to SNS — with its manifest, the commands to build, synth and deploy, and what the synthesized template contains.
markdownexamplev4SB9W-GKFYstage: buildtrust: officialservice: sqsservice: lambdaservice: cloudwatchservice: snsservice: cdkinterface: iacexample projectCDK TypeScript exampleSQS Lambda worker examplepartial batch response exampleDLQ alarmerror rate alarm CDKLambda JSON logging CDKlog group retention CDKcdk synth example
Example: SQS worker on Lambda with DLQ and alarms
Copy the files below into an empty folder, then run npm install and npm run synth. Run npm run deploy only after the account and Region are bootstrapped (guides/cdk/bootstrap-and-deploy.md). Every rule this project applies is explained in patterns/sqs-to-lambda.md and guides/cloudwatch/create-alarms.md.
Manifest
| Item | Value |
|---|---|
| Builds with | Node.js 24, TypeScript strict, aws-cdk-lib 2.273.0, CDK CLI 2.1145.0 |
| Creates | 2 SQS queues (work + DLQ, SSE-SQS), 1 Lambda function (Node.js 24, JSON logs), 1 log group (30 days), an event source mapping, 2 alarms, 1 SNS topic (+ email subscription if ALARM_EMAIL is set) |
| Inputs | CDK_DEFAULT_ACCOUNT / CDK_DEFAULT_REGION (set by the CDK CLI from your profile), optional ALARM_EMAIL |
| Verified | tsc --noEmit passes. The app was bundled and synthesized with aws-cdk-lib directly (equivalent to cdk synth; the npx tsx entry point in cdk.json wasn't run here), giving visibility timeout 180 s, retention 4 days / 14 days, maxReceiveCount 5, FunctionResponseTypes: ["ReportBatchItemFailures"], MaximumConcurrency 10, LogFormat: JSON, log retention 30 days, both alarms notBreaching → SNS |
| Not verified | An actual deployment (no AWS account in the build environment) |
| Cost shape | SQS requests, Lambda requests + GB-seconds, CloudWatch alarms and logs, SNS deliveries. No prices typed here |
Files
package.json
{
"name": "orders-worker",
"private": true,
"type": "module",
"scripts": {
"build": "tsc --noEmit && esbuild src/worker.ts --bundle --platform=node --target=node24 --format=esm --outfile=dist/worker/index.mjs",
"synth": "npm run build && cdk synth",
"deploy": "npm run build && cdk deploy --require-approval broadening"
},
"dependencies": { "aws-cdk-lib": "2.273.0", "constructs": "^10.0.0" },
"devDependencies": { "aws-cdk": "2.1145.0", "@types/aws-lambda": "^8.10.0", "@types/node": "^24.0.0", "esbuild": "^0.25.0", "tsx": "^4.0.0", "typescript": "^5.6.0" }
}
tsconfig.json
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": true, "types": ["node"] }, "include": ["bin", "lib", "src"] }
cdk.json
{ "app": "npx tsx bin/app.ts" }
bin/app.ts
import { App } from "aws-cdk-lib";
import { OrdersWorkerStack } from "../lib/stack.js";
const app = new App();
new OrdersWorkerStack(app, "OrdersWorker", {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
alarmEmail: process.env.ALARM_EMAIL,
});
lib/stack.ts
import { Duration, Stack, type StackProps, RemovalPolicy, CfnOutput } from "aws-cdk-lib";
import type { Construct } from "constructs";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import * as sns from "aws-cdk-lib/aws-sns";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { EmailSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
export interface OrdersWorkerProps extends StackProps { alarmEmail?: string }
export class OrdersWorkerStack extends Stack {
constructor(scope: Construct, id: string, props: OrdersWorkerProps) {
super(scope, id, props);
const timeout = Duration.seconds(30);
const dlq = new sqs.Queue(this, "OrdersDlq", {
retentionPeriod: Duration.days(14), // longer than the source queue
encryption: sqs.QueueEncryption.SQS_MANAGED,
});
const queue = new sqs.Queue(this, "Orders", {
visibilityTimeout: Duration.seconds(timeout.toSeconds() * 6),
retentionPeriod: Duration.days(4),
deadLetterQueue: { queue: dlq, maxReceiveCount: 5 },
encryption: sqs.QueueEncryption.SQS_MANAGED, // SNS/EventBridge/S3 can still deliver later
});
const logGroup = new logs.LogGroup(this, "WorkerLogs", {
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: RemovalPolicy.DESTROY,
});
const fn = new lambda.Function(this, "Worker", {
runtime: lambda.Runtime.NODEJS_24_X,
handler: "index.handler",
code: lambda.Code.fromAsset("dist/worker"),
timeout,
memorySize: 256,
logGroup,
loggingFormat: lambda.LoggingFormat.JSON,
});
fn.addEventSource(new SqsEventSource(queue, { batchSize: 10, reportBatchItemFailures: true, maxConcurrency: 10 }));
const topic = new sns.Topic(this, "Alarms");
if (props.alarmEmail) topic.addSubscription(new EmailSubscription(props.alarmEmail)); // must be confirmed
const errorRate = new cloudwatch.MathExpression({
expression: "100 * FILL(errors, 0) / invocations",
usingMetrics: {
errors: fn.metricErrors({ statistic: "Sum", period: Duration.minutes(1) }),
invocations: fn.metricInvocations({ statistic: "Sum", period: Duration.minutes(1) }),
},
label: "Error %",
}).createAlarm(this, "ErrorRate", {
threshold: 5, evaluationPeriods: 5, datapointsToAlarm: 3,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
const dlqDepth = dlq.metricApproximateNumberOfMessagesVisible({ period: Duration.minutes(1) }).createAlarm(this, "DlqNotEmpty", {
threshold: 0, evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
for (const a of [errorRate, dlqDepth]) a.addAlarmAction(new SnsAction(topic));
new CfnOutput(this, "QueueUrl", { value: queue.queueUrl });
new CfnOutput(this, "DlqUrl", { value: dlq.queueUrl });
}
}
src/worker.ts
import type { SQSEvent, SQSBatchResponse, SQSBatchItemFailure } from "aws-lambda";
type Order = { orderId: string; total: number };
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const batchItemFailures: SQSBatchItemFailure[] = [];
for (const record of event.Records) {
try {
const order = JSON.parse(record.body) as Order;
if (typeof order.orderId !== "string") throw new Error("missing orderId");
// Idempotent work keyed on order.orderId goes here (e.g. a DynamoDB conditional put).
console.info(JSON.stringify({ msg: "processed", orderId: order.orderId, messageId: record.messageId }));
} catch (err) {
console.error(JSON.stringify({ msg: "failed", messageId: record.messageId, error: String(err) }));
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
After deploying
- Email subscription: confirm it, or alarm emails never arrive.
- Topic policy: the CDK added no topic policy for CloudWatch. If alarm notifications don't arrive, add the
cloudwatch.amazonaws.comstatement fromreference/sns/errors.md. Whether same-account alarms need it wasn't settled in the collected sources (UNVERIFIED). - Smoke test:
aws sqs send-message --queue-url <QueueUrl> --message-body '{"orderId":"o-1","total":10}'. Thenaws logs tail <WorkerLogs log-group name> --since 5m. The group has a generated name; find it in the stack's resources. - Failure path: send
{"total":10}. The worker reports it as failed. After 5 receives it moves to the DLQ, and theDlqNotEmptyalarm fires.