AWS Lambda skill for Codex
Faster cold starts, no reprocessed batches. Codex tunes Lambda functions from REPORT log numbers rather than guesses: init duration, billed duration and max memory drive packaging, memory and architecture choices. It keeps handlers thin with clients created at init, gives every trigger a defined retry and dead-letter path, and uses reserved or event-source concurrency to keep downstream databases alive.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- aws-lambda Installs to ~/.agents/skills/aws-lambda/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install aws-lambda The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project aws-lambda Installs to .agents/skills/aws-lambda/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/aws-lambda.zipunzip -o aws-lambda.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) aws-lambda PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $aws-lambda, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds, packages and tunes AWS Lambda functions in Node.js, Python, Java and Go - handler structure, zip vs container image packaging, layers, cold start reduction with SnapStart and provisioned concurrency, memory right-sizing, reserved concurrency, SQS partial batch failures and timeouts. Use when writing a Lambda handler, the function is slow to start or times out, the deployment package is too large, throttling or concurrency errors appear, or a queue-triggered function reprocesses messages.
Use it when
- Writing or refactoring a Lambda handler in Node.js, Python, Java or Go
- Cold starts, timeouts or out-of-memory errors in a function
- A Lambda overwhelms a database or gets throttled
- A queue or stream trigger retries the wrong records
Use something else when
- You need the SAM template, local emulation and deploy pipeline: use aws-sam
- The work is the HTTP layer in front of the function: use aws-api-gateway
How to check the result
Invoke with a representative event and compare Init Duration, p99 Duration and Max Memory Used from REPORT lines before and after the change; a poison SQS message lands alone in the DLQ.
The mistake it prevents
Throttling an SQS consumer with reserved concurrency instead of the event source mapping's maximum concurrency, which bounces messages into the DLQ.
Done well means: the handler does per-request work only, init is small and measured, memory is chosen from data rather than defaults, every trigger has a defined failure path (retry, DLQ or destination), and concurrency limits protect the downstream systems.
Before changing anything
- Read
AGENTS.mdand find how the function is deployed (CDK, SAM, Terraform, Serverless Framework, plain zip). Change the IaC, not the console. - Note runtime, architecture, memory, timeout, trigger type and whether it is in a VPC.
- Pull real numbers if credentials and network are available:
aws lambda get-function-configuration --function-name orders-api \
--query '{rt:Runtime,arch:Architectures,mem:MemorySize,timeout:Timeout,layers:Layers[].Arn}'
aws logs filter-log-events --log-group-name /aws/lambda/orders-api \
--filter-pattern '"REPORT"' --max-items 50 --query 'events[].message'
REPORT lines carry Duration, Billed Duration, Max Memory Used and, on cold
starts, Init Duration. These are the inputs for every tuning decision below.
Handler structure
Create clients, read config and load models outside the handler so warm invocations reuse them. Keep the handler thin and testable.
import json, os
import boto3
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"]) # init once
@logger.inject_lambda_context
def handler(event: dict, context: LambdaContext) -> dict:
order = json.loads(event["body"])
table.put_item(Item=order, ConditionExpression="attribute_not_exists(pk)")
return {"statusCode": 201, "body": json.dumps({"id": order["pk"]})}
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const ddb = new DynamoDBClient({}); // init once, reused while warm
export const handler = async (event: APIGatewayProxyEventV2) => {
// per-request work only
};
- Node.js: use ESM or CommonJS consistently, and the AWS SDK v3 modular clients. The SDK v3 is in the managed runtime, but bundle your own pinned copy for reproducible behaviour.
- Never store per-request data in module scope; warm containers serve many requests.
- Read
context.get_remaining_time_in_millis()(Python) orcontext.getRemainingTimeInMillis()(Node) before starting long work.
Packaging
| Situation | Package as |
|---|---|
| Code plus dependencies under 250 MB unzipped | Zip, bundled (esbuild for Node, pip install -t for Python) |
| Native deps, ML models, >250 MB, or you want one build artifact for many tools | Container image (up to 10 GB) |
| Shared dependency used by many functions, changes rarely | Layer, versioned, max 5 per function |
| Shared code that changes often | Not a layer: bundle it into each function |
Limits to design around: 50 MB zipped for direct upload (use S3 above that), 250 MB
unzipped including layers, 6 MB synchronous request/response payload, 1 MB asynchronous
event payload, 15-minute timeout, 128 MB to 10,240 MB memory, /tmp 512 MB default and
up to 10,240 MB.
Build Python dependencies for the Lambda platform, not your laptop:
pip install -r requirements.txt -t build/ \
--platform manylinux2014_aarch64 --only-binary=:all: --python-version 3.13
Match --platform to the function architecture (manylinux2014_x86_64 for x86_64).
Installing packages needs network; ask before running it in a sandbox.
Cold starts
Measure first: compare Init Duration against the latency budget. Then work down this
list, cheapest first.
- Shrink init. Bundle and tree-shake (esbuild
--minify, exclude unused SDK clients), lazy-import heavy modules used by few code paths, avoid reading large files at import. - Raise memory. CPU scales with memory; 1,769 MB gives one full vCPU. Init and duration often drop enough that cost stays flat.
- arm64. Usually cheaper per GB-second and as fast or faster; test native deps.
- SnapStart for Java, Python and .NET managed runtimes: snapshots the initialized environment. Requires published versions or aliases; re-seed randomness and re-establish network connections in the runtime hooks, because every restored environment starts from the same memory image.
- Provisioned concurrency on an alias for strict p99 latency. Costs money even when idle; schedule it with Application Auto Scaling for business hours.
Note that the init phase is billed for on-demand functions, so a heavy init costs on every cold start, not just latency.
VPC attachment no longer adds the multi-second cold start it once did, but the function needs NAT or VPC endpoints to reach AWS APIs and the internet.
Memory right-sizing
Test at several memory sizes with the same payload and pick the knee of the cost curve. AWS Lambda Power Tuning (a Step Functions app) automates this. Rules of thumb:
Max Memory Usednear the limit: raise memory, you are risking OOM.- CPU-bound work: duration falls almost linearly with memory up to about 1.8 GB, so cost barely changes and latency improves.
- I/O-bound work waiting on a database: extra memory buys little; stay small.
Concurrency
- Account default is 1,000 concurrent executions per region (raisable). Each function can scale by 1,000 concurrent executions every 10 seconds.
- Reserved concurrency caps a function and guarantees it capacity. Use it to protect a database: 50 reserved concurrency means at most 50 connections from this function. Setting it to 0 stops all invocations (a kill switch).
- Provisioned concurrency pre-initializes environments; it does not cap anything.
- Throttled synchronous calls return 429 to the caller; throttled async events are retried for up to 6 hours, then go to the failure destination or DLQ.
For relational databases, use RDS Proxy or a small pool size of 1 per environment.
Triggers and failure handling
| Trigger | Retries | Configure |
|---|---|---|
| API Gateway / Function URL (sync) | None by Lambda; caller decides | Timeout below the API’s 29-30 s limit |
| Async (S3, SNS, EventBridge) | 2 retries by default | MaximumRetryAttempts, MaximumEventAgeInSeconds, on-failure destination |
| SQS | Until message expires or hits maxReceiveCount | Redrive policy to a DLQ, partial batch responses |
| Kinesis / DynamoDB Streams | Until record expires; blocks the shard | BisectBatchOnFunctionError, MaximumRetryAttempts, on-failure destination |
SQS partial batch failure: enable ReportBatchItemFailures in the event source mapping’s
FunctionResponseTypes and return only the failed IDs. Without it one bad message makes
the whole batch visible again and the good ones are reprocessed.
def handler(event, context):
failures = []
for record in event["Records"]:
try:
process(json.loads(record["body"]))
except Exception:
logger.exception("failed", extra={"id": record["messageId"]})
failures.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failures}
Set the queue visibility timeout to at least 6 times the function timeout. Use the event source mapping’s maximum concurrency setting (minimum 2) rather than reserved concurrency to throttle an SQS consumer; reserved concurrency throttling makes messages bounce and burn receive counts.
All async and queue triggers are at-least-once. Make handlers idempotent (conditional writes, Powertools idempotency utility with a DynamoDB table).
Local testing
- Unit-test the business logic without Lambda at all; pass a plain dict event.
sam local invoke Fn -e events/sqs.jsonemulates the runtime and needs Docker.- Generate realistic events with
sam local generate-event sqs receive-message.
Gotchas
- Timeout of 3 s (the default) on a function calling a slow API; set it from p99 downstream latency plus headroom, and set SDK client timeouts lower than the function’s.
- A Lambda that writes to the S3 bucket that triggers it loops. Lambda detects some recursive loops and stops them, but use a separate prefix or bucket.
- Log groups default to never expire. Set retention.
- Environment variables are visible to anyone with
lambda:GetFunctionConfiguration. Keep secrets in Secrets Manager or Parameter Store and cache them (Parameters and Secrets Lambda extension or Powertools parameters). $LATESTcannot have provisioned concurrency or SnapStart; publish versions and point an alias at them.- Deleting a function or changing a production alias needs the user’s confirmation.
Verification
- Invoke with a representative event:
aws lambda invoke --function-name f --cli-binary-format raw-in-base64-out --payload file://event.json out.jsonand inspectout.jsonand theFunctionErrorfield. - Compare
Init Duration, p50/p99DurationandMax Memory Usedbefore and after. - For SQS consumers, inject one poison message and confirm only it reaches the DLQ.
Output
Report: configuration changed (memory, timeout, arch, concurrency) with the before/after numbers that justified it, failure path per trigger, and any cost impact (provisioned concurrency, larger memory).
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AWS Lambda"
short_description: "Faster cold starts, no reprocessed batches"
default_prompt: "$aws-lambda Our Lambda cold start is 4 seconds, figure out why and fix it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.