---
cuppy_hub: aws
cuppy_path: examples/sqs-lambda-worker-cdk.md
cuppy_section: docs
cuppy_doc: SB9W-GKFY
cuppy_doc_uuid: 45b5b77b-78da-4e5e-b51c-b621a59d6a51
cuppy_permalink: https://cuppy.io/aws/d/SB9W-GKFY
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/examples/sqs-lambda-worker-cdk
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 4c016708a83ede772ec4cbff53f57b8e2dab4bf09695445d704607f612c76f79
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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`

```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`

```json
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": true, "types": ["node"] }, "include": ["bin", "lib", "src"] }
```

### `cdk.json`

```json
{ "app": "npx tsx bin/app.ts" }
```

### `bin/app.ts`

```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`

```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`

```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.com` statement from `reference/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}'`. Then `aws 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 the `DlqNotEmpty` alarm fires.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
