Cloud Platforms No. 175

AWS Lambda skill for Claude Code

Fast cold starts, safe retries, no silent drops. Claude writes Lambda handlers that create clients once at init, stay well inside timeouts, and return partial batch failures so one bad SQS message does not replay the rest. It measures Init Duration before tuning, chooses between bundling, arm64, SnapStart and provisioned concurrency, and makes sure every async trigger has a failure destination and alarms.

  • nameaws-lambda
  • size184 lines · ≈2,250 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds and tunes AWS Lambda functions - handler structure with init outside the handler, cold start reduction (bundling, arm64, SnapStart, provisioned concurrency), memory and timeout sizing, packaging with esbuild, layers or container images, configuration and secrets, event source mappings for SQS, Kinesis, DynamoDB Streams and API Gateway, partial batch failures, idempotency and Powertools for logging, tracing and metrics. Use when writing or debugging a Lambda function, fixing cold starts, timeouts or throttling, or wiring a queue or API to Lambda.

Try asking

  • Our Node Lambda behind API Gateway has 3-second cold starts. Fix it
  • Wire this SQS queue to a Lambda with retries and a dead-letter queue
  • Why does our Lambda process the same S3 upload twice?

Use it when

  • Writing or reviewing Lambda handler code
  • Cold starts, timeouts or throttles are hurting latency
  • Connecting SQS, streams, S3 or API Gateway to a function
  • Adding structured logging, tracing and idempotency

Skip it when

  • The issue is an AccessDenied or role policy — use aws-iam
  • You are defining the whole stack in CDK — use aws-cdk alongside this

How to check the result

Invoking the alias succeeds with p99 duration well under the timeout, a deliberately failing event reaches the DLQ or destination, and alarms exist for errors, throttles and queue depth.

The common mistake it prevents

Catching every exception and returning success, which defeats Lambda retries and leaves the dead-letter queue empty while data is lost.

~/.claude/skills/aws-lambda/SKILL.md

--- name: aws-lambda description: Builds and tunes AWS Lambda functions - handler structure with init outside the handler, cold start reduction (bundling, arm64, SnapStart,… license: MIT ---

A production Lambda initializes clients once, finishes well inside its timeout, emits structured logs with the request ID, tolerates duplicate and partial-batch delivery, has a dead-letter or failure destination for every async source, and is sized from measured duration and memory rather than defaults.

Workflow

- [ ] 1. Identify runtime, architecture, memory, timeout, trigger(s), IaC tool (SAM, CDK, Terraform, Serverless)
- [ ] 2. Read the handler: what runs at init vs per invocation
- [ ] 3. Check the event source contract (batching, retries, failure handling)
- [ ] 4. Fix the code: init outside handler, idempotency, partial batch responses
- [ ] 5. Package small; configure env/secrets; set memory/timeout from measurements
- [ ] 6. Add Powertools logging/tracing/metrics and alarms
- [ ] 7. Test locally and invoke in a dev account; check logs and metrics

Handler shape

// Node.js 22 (ESM). Init code runs once per execution environment.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { Logger } from "@aws-lambda-powertools/logger";
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2, Context } from "aws-lambda";

const logger = new Logger({ serviceName: "orders" });
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME!;

export const handler = async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
  logger.addContext(context);
  const id = event.pathParameters?.id;
  if (!id) return { statusCode: 400, body: JSON.stringify({ error: "id required" }) };
  const { Item } = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk: `ORDER#${id}` } }));
  return Item
    ? { statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify(Item) }
    : { statusCode: 404, body: JSON.stringify({ error: "not found" }) };
};
# Python 3.13
import json, os
import boto3
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger(service="orders")
tracer = Tracer()
table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])   # created once

@logger.inject_lambda_context
@tracer.capture_lambda_handler
def handler(event: dict, context: LambdaContext) -> dict:
    item = table.get_item(Key={"pk": f"ORDER#{event['pathParameters']['id']}"}).get("Item")
    return {"statusCode": 200 if item else 404, "body": json.dumps(item or {"error": "not found"}, default=str)}

Never leave work running after the handler returns: the environment freezes, and timers, unawaited promises or background threads resume (or vanish) in a later invocation. Global state persists between invocations in the same environment: good for clients and caches, wrong for per-request data.

Limits that shape designs

LimitValue
Timeoutmax 900 s (15 min)
Memory128 MB-10,240 MB; CPU scales with memory (about 1 vCPU at 1,769 MB)
Sync payload (request and response)6 MB
Zip package50 MB zipped direct upload, 250 MB unzipped including layers
Container image10 GB
/tmp512 MB default, up to 10,240 MB
Env vars4 KB total
API Gateway REST/HTTP integration timeout29 s default (REST regional can be raised by quota request); HTTP APIs 30 s

Work longer than these belongs in Step Functions, ECS/Fargate tasks, or a queue-driven chain.

Cold starts

Measure first: CloudWatch REPORT lines include Init Duration for cold starts.

aws logs filter-log-events --log-group-name /aws/lambda/orders-api \
  --filter-pattern '"Init Duration"' --max-items 20 --query 'events[].message'
LeverEffect
Bundle and tree-shake (esbuild), import only needed SDK v3 clientsFewer files to load; often the largest win for Node
Lazy-import rarely used heavy modulesShorter init for common paths
More memoryMore CPU during init and execution; often cheaper overall when duration drops
arm64 (Graviton)About 20% cheaper per GB-second; similar or better performance for most code
SnapStart (Java 11+, Python 3.12+, .NET 8+)Restores from a snapshot; requires published versions and care with uniqueness (re-seed randomness, refresh credentials/connections in restore hooks)
Provisioned concurrency on an aliasPre-initialized environments; pay for idle; use with Application Auto Scaling schedules
Avoid VPC unless neededVPC cold start penalty is mostly gone (Hyperplane ENIs), but NAT and DNS add latency and cost

Use AWS Lambda Power Tuning (open-source Step Functions state machine) to find the memory size with the best cost/duration trade-off.

Packaging and configuration

  • Node: bundle with esbuild (--bundle --platform=node --target=node22 --format=esm, plus a createRequire banner if CJS deps need require). The runtime includes AWS SDK v3, but bundling your own pins the version you tested.
  • Python: build dependencies for the target architecture (pip install --platform manylinux2014_aarch64 --only-binary=:all: -t build/ -r requirements.txt) or use container images / SAM sam build --use-container.
  • Config via environment variables; secrets via SSM Parameter Store or Secrets Manager fetched at init and cached (Powertools Parameters utility, or the Parameters and Secrets Lambda Extension). Never put secrets in plain env vars visible in the console if policy forbids it.
  • Publish versions and point an alias (live) at them; deploy with CodeDeploy traffic shifting (SAM DeploymentPreference) for gradual rollout with alarms.

Event sources and failure handling

SourceInvocationRetry behaviorMust configure
API Gateway / Function URL / ALBSyncCaller decidesTimeouts, error mapping
S3, SNS, EventBridgeAsync2 retries by default, then droppedOn-failure destination or DLQ, MaximumEventAgeInSeconds, MaximumRetryAttempts
SQSPoll (event source mapping)Message returns after visibility timeout until maxReceiveCount, then DLQVisibility timeout >= 6x function timeout, ReportBatchItemFailures, redrive policy, MaximumConcurrency
Kinesis / DynamoDB StreamsPoll, ordered per shardRetries the batch until success or expiry, blocking the shardBisectBatchOnFunctionError, MaximumRetryAttempts, on-failure destination, ReportBatchItemFailures

Partial batch failure for SQS (Powertools batch utility):

from aws_lambda_powertools.utilities.batch import BatchProcessor, EventType, process_partial_response

processor = BatchProcessor(event_type=EventType.SQS)

def record_handler(record):
    process_order(json.loads(record.body))      # raise to mark only this record failed

def handler(event, context):
    return process_partial_response(event=event, record_handler=record_handler,
                                    processor=processor, context=context)

All sources deliver at least once: make handlers idempotent (Powertools Idempotency utility with a DynamoDB table, keyed on a stable event field). For detailed per-source settings and sample events, read references/event-sources.md.

Concurrency

  • Account concurrency default is 1,000 per region (can be lower on new accounts; request increases). One hot function can starve others: set reserved concurrency on critical and on noisy functions.
  • Reserved concurrency also caps downstream load (e.g. database connections). For relational databases use RDS Proxy or connection-light drivers.
  • Throttles (Throttles metric) on async sources are retried automatically; on sync sources they surface as 429/502 to callers.

Gotchas

  • Recursive loops (Lambda writes to the S3 bucket or queue that triggers it) burn money fast; Lambda’s recursive loop detection stops some (SQS, SNS, S3) but design them out.
  • A handler that catches all exceptions and returns normally hides failures from retries and DLQs.
  • context.getRemainingTimeInMillis() lets long loops stop and checkpoint before timeout.
  • Timeouts kill the environment; the next invocation cold-starts.
  • Logging full events can leak PII and cost a lot in CloudWatch Logs; set log retention (never “Never expire”) and log selectively.

Verification

sam local invoke OrdersFunction -e events/get-order.json        # local, if SAM
aws lambda invoke --function-name orders-api:live \
  --cli-binary-format raw-in-base64-out --payload file://events/get-order.json out.json && cat out.json
aws logs tail /aws/lambda/orders-api --follow --since 10m

Confirm: no errors in logs, duration well under the timeout at p99, Init Duration improved if cold starts were the target, DLQ/destination receives a deliberately failing event, and alarms exist on Errors, Throttles, DLQ depth, and (for streams) IteratorAge.

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/event-sources.md 99 lines · 4.5 KB
# Event source details

Read the section for the trigger you are wiring.

## SQS

- Batch size up to 10,000 for standard queues (with `MaximumBatchingWindowInSeconds`),
  up to 10 for FIFO. Start with 10 and a 0-5 s window.
- Visibility timeout of the queue: at least 6x the function timeout (AWS guidance), so
  messages are not redelivered while a retried invocation is still running.
- Turn on `FunctionResponseTypes: [ReportBatchItemFailures]` and return
  `{"batchItemFailures": [{"itemIdentifier": "<messageId>"}]}`. Without it, one bad
  message makes the whole batch retry, duplicating the successful ones.
- Redrive policy on the source queue: `maxReceiveCount` 3-5 to a DLQ. Alarm on the DLQ's
  `ApproximateNumberOfMessagesVisible > 0`.
- `ScalingConfig.MaximumConcurrency` (2-1000) caps concurrent invocations per event
  source mapping without reserved concurrency side effects (throttling-induced retries).
- FIFO: ordering per `MessageGroupId`; a failing message blocks its group until it
  succeeds or goes to the DLQ.

SAM:

```yaml
OrdersWorker:
  Type: AWS::Serverless::Function
  Properties:
    Timeout: 30
    Events:
      Queue:
        Type: SQS
        Properties:
          Queue: !GetAtt OrdersQueue.Arn
          BatchSize: 10
          MaximumBatchingWindowInSeconds: 2
          FunctionResponseTypes: [ReportBatchItemFailures]
          ScalingConfig: { MaximumConcurrency: 20 }
OrdersQueue:
  Type: AWS::SQS::Queue
  Properties:
    VisibilityTimeout: 180
    RedrivePolicy: { deadLetterTargetArn: !GetAtt OrdersDLQ.Arn, maxReceiveCount: 5 }
```

## Kinesis and DynamoDB Streams

- Records are ordered per shard; a failing batch blocks the shard until success or
  record expiry (24 h default for Kinesis, 24 h for DynamoDB Streams).
- Always set: `MaximumRetryAttempts` (e.g. 3), `MaximumRecordAgeInSeconds`,
  `BisectBatchOnFunctionError: true`, `DestinationConfig.OnFailure` (SQS or SNS) to
  capture metadata of dropped batches, and `ReportBatchItemFailures` (return the
  sequence number of the first failed record).
- `ParallelizationFactor` (1-10) processes multiple batches per shard concurrently while
  keeping per-partition-key order.
- Alarm on `IteratorAge`: growing age means the consumer is falling behind or stuck.
- Event filtering (`FilterCriteria`) drops irrelevant records before invocation (cheaper).

## API Gateway and Function URLs

- HTTP API (v2 payload) is cheaper and simpler than REST API; REST API adds usage plans,
  API keys, request validation, WAF integration on more configurations, and private APIs.
- Payload format 2.0: `event.requestContext.http.method`, `event.rawPath`, cookies in
  `event.cookies`; return `{statusCode, headers, body, cookies}` or a plain object (HTTP API
  serializes it as JSON with 200).
- Binary responses: set `isBase64Encoded: true` and base64 the body.
- Function URLs: simplest public HTTPS endpoint; auth `AWS_IAM` or `NONE`; supports
  response streaming (Node.js managed runtime) for long or incremental responses.
- Integration timeout is the hard ceiling for synchronous work; push longer tasks to a queue
  and return 202.

## S3 events

- Delivered asynchronously, at least once, possibly out of order and possibly delayed.
- Object keys in events are URL-encoded (spaces become `+`): decode with
  `decodeURIComponent(key.replace(/\+/g, " "))` or `urllib.parse.unquote_plus(key)`.
- Use EventBridge notifications for S3 when you need filtering by more than prefix/suffix
  or multiple consumers.
- Never write output to the same bucket/prefix that triggers the function without a
  prefix filter that excludes outputs.

## EventBridge and SNS

- Async invocation: configure `MaximumRetryAttempts` (0-2), `MaximumEventAgeInSeconds`,
  and an on-failure destination (SQS/SNS/EventBridge/Lambda/S3). Destinations carry the
  full request and error context; prefer them to the legacy DLQ setting.
- EventBridge rules can also retry and use their own DLQ before Lambda's async queue.
- EventBridge Scheduler (not cron rules) for one-off and timezone-aware schedules.

## Sample events

Generate realistic test events instead of hand-writing them:

```bash
sam local generate-event sqs receive-message --body '{"orderId":"o1"}' > events/sqs.json
sam local generate-event apigateway http-api-proxy > events/http.json
sam local generate-event s3 put --bucket my-bucket --key uploads/a.png > events/s3.json
```

Powertools also ships typed event classes (Python `aws_lambda_powertools.utilities.data_classes`,
TypeScript parser schemas) for parsing and validating each source.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc