AWS Lambda just got two upgrades that change how you should build serverless apps in 2026: a 4x jump in async payload size and a new way to deploy code straight from S3 without copying it into Lambda’s storage quota. Combined with durable functions wired into Amazon Bedrock, Lambda in August 2026 looks less like “just run some code” and more like the default fabric for event-driven, AI-ready systems. This tutorial walks through building a real serverless API from scratch, using the current runtime limits, the new payload ceiling, and the deployment pattern AWS shipped in July 2026. By the end you’ll have a working task-management API backed by DynamoDB, a scheduled EventBridge job, provisioned concurrency tuned to kill cold starts, and a Bedrock-connected endpoint that summarizes incoming data. No prior serverless experience required, but you should be comfortable in a terminal.
What Changed in AWS Lambda for 2026
Three changes matter most if you’re setting up AWS Lambda today. First, AWS raised the maximum payload size for asynchronous invocations, Amazon SQS, and Amazon EventBridge from 256 KB to 1 MB, according to the Q1 2026 Serverless ICYMI roundup. That single change removes a lot of the chunking logic developers used to bolt onto event-driven pipelines just to stay under the old ceiling. Second, the Q2 2026 update highlighted Lambda durable functions as the standard pattern for reliable, long-running workflows tied to Amazon Bedrock, aimed at voice analytics and other AI pipelines that need to survive retries without losing state. Third, and most recently, a July 2026 AWS Compute Blog post introduced self-managed Amazon S3 buckets for Lambda deployment packages, with a new S3ObjectStorageMode configuration attribute.
That last one is worth pausing on. Historically, when you pointed Lambda at an S3 object for your deployment package, Lambda copied that object into its own internal storage, and the copy counted against your account’s Lambda code storage quota. With S3ObjectStorageMode set to REFERENCE instead of the default COPY, Lambda reads your code directly from your own bucket and skips the copy entirely. That’s useful once you’re shipping large dependency bundles, ML model artifacts, or anything close to the 250 MB unzipped package ceiling. It’s available in every standard AWS region where Lambda runs, and AWS says it adds no cost beyond normal S3 storage and request pricing. This tutorial uses that pattern for deployment in Step 4.
None of this changes what Lambda fundamentally is: a serverless compute service that runs your code in response to events without you provisioning or managing servers, as AWS’s own Lambda developer guide puts it. What’s changed is the scope of what people are building on it. A 2026 serverless architecture analysis frames it plainly: serverless is no longer just “Lambda plus API Gateway,” it’s become the default fabric for event-driven systems, real-time experiences, and AI applications that need to stream results fast. That’s the app we’re building below.
Why does any of this matter if you’ve never touched Lambda before? Because the on-ramp used to be steeper than it needed to be. A few years ago, building a production-grade serverless API meant hand-rolling payload chunking for large events, writing custom warm-up pings to fight cold starts, and treating AI model calls as a bolt-on rather than a first-class part of the architecture. The 2026 changes collapse a lot of that scaffolding into defaults you configure once. That’s the real reason this is worth learning now rather than treating Lambda as a settled, mature service with nothing new to say. The steps below build a small but complete system that touches every one of these changes directly, not a toy “hello world” that stops short of anything you’d actually ship.
Prerequisites
Get these installed and configured before Step 1. Skipping any of them is the single biggest reason people get stuck halfway through a Lambda tutorial.
- An AWS account with billing enabled (the free tier covers everything in this tutorial)
- AWS CLI v2, latest version, configured with an IAM user or role that has programmatic access
- Node.js 20.x (matches one of Lambda’s currently supported managed runtimes) and npm
- AWS SAM CLI, latest version, for local packaging and deployment
- A terminal with curl or Postman for testing HTTP endpoints
- Basic familiarity with JSON and IAM policy syntax
- About 45-60 minutes of uninterrupted time
Verify your CLI setup before moving on:
aws --version
# aws-cli/2.x.x Python/3.x.x
aws sts get-caller-identity
# {
# "UserId": "AIDAEXAMPLE123456",
# "Account": "123456789012",
# "Arn": "arn:aws:iam::123456789012:user/your-user"
# }
sam --version
# SAM CLI, version 1.x.x
If aws sts get-caller-identity throws an error instead of returning your account ID, stop here and run aws configure first. Nothing downstream will work without valid credentials.
Step 1: Configure IAM Permissions the Right Way
Resist the urge to attach AdministratorAccess to your deployment user and move on. Lambda functions need an execution role, which is separate from the IAM user or role you use to deploy. Create a dedicated deployment policy scoped to what this tutorial actually touches: Lambda, IAM role creation, API Gateway, DynamoDB, EventBridge, and CloudWatch Logs.
aws iam create-policy \
--policy-name LambdaTutorialDeployPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:*",
"apigateway:*",
"dynamodb:*",
"events:*",
"logs:*",
"iam:PassRole",
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": "*"
}
]
}'
Yes, the Resource: "*" is broad for a tutorial. In production, scope each action down to specific ARNs and drop the wildcard actions in favor of explicit ones. For a learning environment tied to a throwaway AWS account, this is a reasonable starting point that still avoids full admin rights.
Step 2: Create the Lambda Execution Role
The execution role is what your function assumes at runtime, distinct from the deployment permissions above. It needs a trust policy that lets Lambda assume it, plus permissions for whatever AWS services your function code touches.
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
EOF
aws iam create-role \
--role-name lambda-tutorial-exec-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name lambda-tutorial-exec-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam attach-role-policy \
--role-name lambda-tutorial-exec-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
The AWSLambdaBasicExecutionRole managed policy grants write access to CloudWatch Logs, which you need for every function regardless of what it does. Attaching AmazonDynamoDBFullAccess is fine here for speed. Swap it for a scoped custom policy once you move past learning mode.
Step 3: Write Your First Lambda Function
Create a project directory and a minimal handler. This function will become the core of a task API, but start simple so you can confirm the deployment pipeline works before adding complexity.
mkdir lambda-tutorial-2026 && cd lambda-tutorial-2026
mkdir src && cd src
npm init -y
cat > index.mjs << 'EOF'
export const handler = async (event) => {
console.log("Received event:", JSON.stringify(event));
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "Lambda is live",
timestamp: new Date().toISOString()
})
};
};
EOF
cd ..
Using .mjs tells the Node.js 20.x runtime to treat this as an ES module, which is the default style for new Lambda functions going into 2026. Package it into a deployment zip:
cd src && zip -r ../function.zip . -x "*.git*" && cd ..
Step 4: Deploy Using Self-Managed S3 Storage
This is where the July 2026 change comes in. Instead of uploading your zip directly to Lambda (which copies it into Lambda's internal storage and eats into your code storage quota), upload it to your own S3 bucket and reference it with S3ObjectStorageMode set to REFERENCE.
BUCKET_NAME="lambda-tutorial-$(aws sts get-caller-identity --query Account --output text)"
aws s3 mb s3://$BUCKET_NAME --region us-east-1
aws s3 cp function.zip s3://$BUCKET_NAME/function.zip
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws lambda create-function \
--function-name tutorial-task-api \
--runtime nodejs20.x \
--handler index.handler \
--role arn:aws:iam::${ACCOUNT_ID}:role/lambda-tutorial-exec-role \
--code S3Bucket=$BUCKET_NAME,S3Key=function.zip,S3ObjectStorageMode=REFERENCE \
--timeout 10 \
--memory-size 256
With S3ObjectStorageMode=REFERENCE, Lambda reads your code directly from that bucket at invocation time rather than duplicating it. That means every future update just needs a new aws s3 cp and an aws lambda update-function-code call pointing at the same reference, without inflating your account's Lambda storage usage. If you leave the parameter off entirely, Lambda falls back to the default COPY mode, which still works fine for small functions like this one but doesn't scale as cleanly once you're bundling larger dependencies or model files.
Step 5: Test and Invoke the Function
Invoke it directly from the CLI before wiring up any triggers. This isolates deployment problems from integration problems.
aws lambda invoke \
--function-name tutorial-task-api \
--payload '{}' \
--cli-binary-format raw-in-base64-out \
response.json
cat response.json
Expected output:
{
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": "{\"message\":\"Lambda is live\",\"timestamp\":\"2026-08-17T14:32:09.441Z\"}"
}
If you get a StatusCode other than 200 in the response envelope, check response.json for a FunctionError field first, then jump to the troubleshooting section below.
Step 6: Create a DynamoDB Table for Task Storage
Now give the function something real to do. Create a DynamoDB table to back a small task-management API, the kind of CRUD workload Lambda handles well because each request is short-lived and stateless.
aws dynamodb create-table \
--table-name Tasks \
--attribute-definitions AttributeName=taskId,AttributeType=S \
--key-schema AttributeName=taskId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
PAY_PER_REQUEST billing mode matters here: it avoids provisioning fixed read/write capacity units you'd otherwise pay for even when the table is idle, which fits the serverless, pay-for-what-you-use model this whole tutorial is built around.
Step 7: Write the CRUD Logic
Replace index.mjs with a handler that reads the HTTP method and path from the incoming event and routes to the matching DynamoDB operation.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, GetCommand, ScanCommand, DeleteCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = "Tasks";
export const handler = async (event) => {
const method = event.requestContext?.http?.method || event.httpMethod;
try {
if (method === "POST") {
const body = JSON.parse(event.body || "{}");
const task = { taskId: randomUUID(), title: body.title, done: false, createdAt: new Date().toISOString() };
await docClient.send(new PutCommand({ TableName: TABLE_NAME, Item: task }));
return respond(201, task);
}
if (method === "GET") {
const result = await docClient.send(new ScanCommand({ TableName: TABLE_NAME }));
return respond(200, result.Items);
}
if (method === "DELETE") {
const taskId = event.pathParameters?.taskId;
await docClient.send(new DeleteCommand({ TableName: TABLE_NAME, Key: { taskId } }));
return respond(204, {});
}
return respond(405, { error: "Method not allowed" });
} catch (err) {
console.error("Handler error:", err);
return respond(500, { error: "Internal error" });
}
};
function respond(statusCode, body) {
return { statusCode, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
}
Notice the handler checks both event.requestContext?.http?.method and the older event.httpMethod field. That's not defensive paranoia for its own sake: API Gateway HTTP APIs and REST APIs shape the event object differently, and Lambda Function URLs shape it differently again. Writing the handler to accept either format means you can swap the trigger later (say, adding a Function URL for quick internal testing) without touching the routing logic.
Package the AWS SDK dependencies into the zip and redeploy using the same S3 reference pattern from Step 4:
cd src
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
zip -r ../function.zip . -x "*.git*"
cd ..
aws s3 cp function.zip s3://$BUCKET_NAME/function.zip
aws lambda update-function-code \
--function-name tutorial-task-api \
--s3-bucket $BUCKET_NAME \
--s3-key function.zip \
--s3-object-storage-mode REFERENCE
Step 8: Connect API Gateway for HTTP Access
A Lambda function isn't reachable over HTTP on its own. Amazon API Gateway sits in front and translates HTTP requests into the event format Lambda expects, as described in the API Gateway developer guide. Use an HTTP API (not REST API) for this tutorial, since it's cheaper and lower-latency for simple proxy integrations.
API_ID=$(aws apigatewayv2 create-api \
--name tutorial-task-api-gw \
--protocol-type HTTP \
--target arn:aws:lambda:us-east-1:${ACCOUNT_ID}:function:tutorial-task-api \
--query ApiId --output text)
aws lambda add-permission \
--function-name tutorial-task-api \
--statement-id apigw-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:${ACCOUNT_ID}:${API_ID}/*/*"
echo "API endpoint: https://${API_ID}.execute-api.us-east-1.amazonaws.com/"
The --target flag on create-api auto-generates a default catch-all route and stage, which is the fastest path for a tutorial. In production you'd define explicit routes per HTTP method and path instead of the wildcard proxy.
Step 9: Eliminate Cold Starts With Provisioned Concurrency
Cold starts remain the primary latency risk in serverless architectures going into 2026, since a function that hasn't been invoked recently needs its execution environment initialized before your code runs. The standard mitigation, per the Lambda provisioned concurrency documentation, is to pre-initialize a set number of execution environments so they're always warm.
aws lambda publish-version --function-name tutorial-task-api
aws lambda put-provisioned-concurrency-config \
--function-name tutorial-task-api \
--qualifier 1 \
--provisioned-concurrent-executions 2
Provisioned concurrency runs roughly $0.015 per GB-hour, a discounted rate compared to standard invocation billing, and you pay for it whether or not the environment gets an actual request. Pair it with edge caching through Amazon CloudFront for latency-sensitive read paths (like the GET route above) so repeat requests never hit Lambda at all. Two provisioned instances is enough for a tutorial. Production APIs typically scale this based on observed traffic patterns from CloudWatch metrics.
Step 10: Add a Scheduled Trigger With EventBridge
Not every Lambda invocation comes from a user request. Amazon EventBridge, the successor to CloudWatch Events, is the standard way to invoke a function on a schedule, effectively replacing a cron daemon running on EC2 for this class of workload, according to AWS's EventBridge documentation. Add a rule that runs a cleanup job every night.
aws events put-rule \
--name nightly-task-cleanup \
--schedule-expression "cron(0 3 * * ? *)"
aws lambda add-permission \
--function-name tutorial-task-api \
--statement-id eventbridge-invoke \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:${ACCOUNT_ID}:rule/nightly-task-cleanup
aws events put-targets \
--rule nightly-task-cleanup \
--targets "Id"="1","Arn"="arn:aws:lambda:us-east-1:${ACCOUNT_ID}:function:tutorial-task-api"
The cron expression above fires at 03:00 UTC daily. Your handler needs to check for a scheduled-event shape (no httpMethod present) and branch into cleanup logic rather than the CRUD routing from Step 7. This is also where the new 1 MB async payload ceiling matters most: scheduled and event-driven invocations route through the async invocation path, so if your cleanup job needs to pass a larger batch of task IDs, you now have four times the headroom you had before Q1 2026.
Step 11: Build an AI-Ready Endpoint With Bedrock
This is the durable-functions pattern AWS highlighted in its Q2 2026 Serverless ICYMI update: pairing Lambda with Amazon Bedrock so a single invocation can call a foundation model, wait on the response, and persist state across retries without losing progress. Add a route that summarizes a task list using Bedrock.
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
const bedrock = new BedrockRuntimeClient({});
async function summarizeTasks(tasks) {
const prompt = `Summarize these tasks in one sentence: ${tasks.map(t => t.title).join(", ")}`;
const command = new InvokeModelCommand({
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
contentType: "application/json",
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 200,
messages: [{ role: "user", content: prompt }]
})
});
const response = await bedrock.send(command);
const payload = JSON.parse(new TextDecoder().decode(response.body));
return payload.content[0].text;
}
Wire this into a new GET /summary route the same way you added the CRUD routes, and grant the execution role from Step 2 bedrock:InvokeModel permission. The pattern AWS describes for production durable functions goes further than this simple example: it checkpoints progress so a multi-step AI workflow (call model, wait, call model again, write results) survives a Lambda timeout or retry without restarting from scratch. That's the piece that makes serverless viable for longer AI-driven workflows instead of just quick synchronous calls, and it's the same underlying pattern AWS describes powering incident-triage agents that consume monitoring alerts with memory and guardrails attached.
Keep in mind that every call to summarizeTasks hits Bedrock's own pricing, billed separately from Lambda's compute charges and based on input and output tokens for the model you invoke. For a low-traffic internal tool, that's a rounding error. For a public-facing endpoint, cache the summary result for a short window (say, 60 seconds in DynamoDB or an in-memory cache scoped to a warm execution environment) rather than calling the model on every single request. That one change is usually the difference between a summary endpoint that costs pennies a day and one that surprises you on the monthly bill.
Step 12: Add Logging, Monitoring, and Alarms
Every Lambda invocation already writes to CloudWatch Logs by default, thanks to the basic execution role policy from Step 2, per the Lambda-CloudWatch integration docs. Close the loop by adding an alarm on error rate so you find out about failures before your users do.
aws cloudwatch put-metric-alarm \
--alarm-name tutorial-task-api-errors \
--metric-name Errors \
--namespace AWS/Lambda \
--statistic Sum \
--period 300 \
--threshold 3 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=FunctionName,Value=tutorial-task-api \
--evaluation-periods 1 \
--treat-missing-data notBreaching
This fires when the function throws more than 3 errors in a 5-minute window. Attach an SNS topic as the alarm action if you want a text or email when it trips. That step is outside the scope of this tutorial but takes about five minutes with aws sns create-topic and aws cloudwatch put-metric-alarm --alarm-actions.
Bonus: Test Changes Locally Before Every Redeploy
Redeploying through S3 for every one-line code change gets tedious fast. The AWS SAM CLI can invoke your function locally in a Docker container that mirrors the real Lambda runtime, so you catch syntax errors and logic bugs before they cost you a deploy cycle. This isn't strictly required for the tutorial to work, but it's the habit that separates people who ship Lambda functions daily from people who dread touching them.
cat > event.json << 'EOF'
{
"requestContext": { "http": { "method": "POST" } },
"body": "{\"title\":\"Test task from local invoke\"}"
}
EOF
sam local invoke -t template.yaml --event event.json
The first run pulls a runtime container image, which takes a minute or two. After that, local invokes are fast enough to run after every meaningful edit. Pair this with sam local start-api if you want a live local endpoint that mimics the whole API Gateway integration, not just a single function call, which catches routing mistakes before they reach the real AWS environment.
Your Complete Working Project
At this point you have a task-management API running entirely on Lambda: a POST route that writes to DynamoDB, a GET route that reads task lists, a DELETE route, a nightly EventBridge-triggered cleanup job, provisioned concurrency keeping the API warm, a CloudWatch alarm watching for errors, and a Bedrock-backed summary endpoint. Nothing here runs on a server you manage, and the deployment package lives in your own S3 bucket via S3ObjectStorageMode=REFERENCE rather than duplicated inside Lambda's storage. Test the full flow end to end:
# Create a task
curl -X POST https://${API_ID}.execute-api.us-east-1.amazonaws.com/ \
-H "Content-Type: application/json" \
-d '{"title":"Write the Lambda tutorial"}'
# List all tasks
curl https://${API_ID}.execute-api.us-east-1.amazonaws.com/
# Get an AI-generated summary
curl https://${API_ID}.execute-api.us-east-1.amazonaws.com/summary
AWS Lambda Pricing and Limits in 2026
Understanding what you're billed for, and where the hard ceilings sit, prevents surprise bills and failed deployments. The table below reflects the current published quotas and pricing structure.
| Resource | Limit or Rate | Notes |
|---|---|---|
| Max execution time | 15 minutes | Per single invocation, unchanged in 2026 |
| Default regional concurrency | 1,000 | Increasable via AWS Support request |
| Max unzipped package size | 250 MB | Includes layers |
| Async/SQS/EventBridge payload | 1 MB | Raised from 256 KB in Q1 2026 |
| Provisioned concurrency | ~$0.015 per GB-hour | Billed whether invoked or idle |
| Supported runtimes | Node.js, Python, Java, Go, Ruby, .NET, custom via Layers | See official runtimes list |
For the exhaustive, always-current numbers, check the official Lambda quotas page and the Lambda pricing page directly, since AWS updates both more frequently than any third-party summary can track.
Two numbers in that table deserve extra attention because they're the ones people actually hit in practice. The 1,000 default regional concurrency limit sounds generous until you remember it's shared across every function in that region on your account, not per function. If you've got a dozen functions and one starts getting hammered by traffic, it can starve the others of available concurrency unless you set reserved concurrency per function to carve out a guaranteed slice. And the 250 MB unzipped package cap catches people who bundle the entire AWS SDK v2 into every function instead of importing just the specific client modules they need, like the @aws-sdk/client-dynamodb import used in Step 7. Modular imports alone can cut package size by more than half compared to importing the whole SDK.
Serverless vs Traditional Compute: When Lambda Fits
Lambda isn't the right tool for every workload. It excels at short, event-driven tasks with unpredictable or spiky traffic: API backends, scheduled jobs, image processing on upload, and now AI-workflow orchestration. It's a worse fit for long-running processes over 15 minutes, workloads needing persistent in-memory state between requests, or extremely latency-sensitive systems where even a provisioned-concurrency warm start isn't fast enough.
The decision usually comes down to traffic shape, not raw performance. A service that gets a steady, predictable stream of requests around the clock often costs less to run on EC2 or Fargate, where you're paying a flat rate for reserved capacity instead of a per-invocation charge. A service with bursty, unpredictable, or mostly-idle traffic (a webhook handler that fires a few hundred times a day, a nightly batch job, an API that spikes during business hours and goes quiet overnight) tends to cost less and require less operational overhead on Lambda, since you're not paying for capacity sitting idle. The task API built in this tutorial is a textbook case for the second category.
| Workload Type | Good Fit for Lambda? | Why |
|---|---|---|
| REST/HTTP API backend | Yes | Short requests, scales to zero |
| Scheduled batch jobs | Yes | EventBridge replaces cron servers |
| Real-time video transcoding | No | Exceeds 15-minute execution cap |
| AI agent orchestration | Yes, with durable functions | Checkpointed state survives retries |
| Stateful multiplayer game server | No | Needs persistent connections and memory |
| Webhook receivers | Yes | Bursty, event-driven by nature |
Common Pitfalls When Setting Up AWS Lambda
These are the mistakes that trip up most first-time Lambda builders, based on the patterns in this tutorial:
- Forgetting IAM PassRole permission. If
create-functionfails with an access-denied error even though your role exists, your deployment user is missingiam:PassRoleon that specific role ARN. - Mixing up execution role and deployment permissions. The role in Step 2 is what your code assumes at runtime. It has nothing to do with the CLI credentials you're deploying with.
- Leaving memory at the default 128 MB. Lambda allocates CPU proportionally to memory, so a memory-starved function is often also CPU-starved, and runs slower than it needs to.
- Ignoring the DynamoDB Scan cost. The
ScanCommandin Step 7 reads every item in the table. It's fine for a tutorial-sized table. Swap it for aQueryCommandwith a proper key condition once your table grows past a few hundred items. - Skipping provisioned concurrency and then blaming Lambda for latency. A cold Node.js function can add several hundred milliseconds of init time. If your API needs consistent sub-100ms responses, Step 9 isn't optional.
- Forgetting to add the API Gateway invoke permission. If your HTTP requests return a 403 with an "internal server error" from API Gateway itself, not your function, check that the
add-permissioncall in Step 8 actually completed. - Deploying secrets as plaintext environment variables. Use AWS Secrets Manager or SSM Parameter Store and fetch secrets at cold-start time, not hardcoded values in your zip.
- Not setting a Bedrock spending guardrail. The summary endpoint in Step 11 calls a foundation model on every request. Without a usage alert or a request cache, a traffic spike or a bug that loops the summary call turns into a real bill fast. Set a CloudWatch billing alarm before you open the endpoint to real traffic.
- Assuming REFERENCE mode means you can delete the S3 object. With
S3ObjectStorageMode=REFERENCE, Lambda reads your code from that bucket at invocation time rather than storing an internal copy. Delete or overwrite the object and the next cold start fails outright.
Troubleshooting Guide
Work through these when something in the tutorial doesn't behave as expected.
- "Unable to import module 'index'": Your zip's folder structure is wrong. The handler file must sit at the root of the zip, not nested inside a subfolder.
- Function times out after exactly 3 seconds: You're hitting the default 3-second timeout. Increase it explicitly with
--timeoutoncreate-functionorupdate-function-configuration. - DynamoDB "ResourceNotFoundException": Your execution role's region doesn't match the table's region, or the table name is misspelled in your handler code.
- API Gateway returns a raw 500 with no body: Check CloudWatch Logs for the function first. This usually means your handler threw an unhandled exception before reaching your try/catch.
- "S3ObjectStorageMode" parameter rejected: Confirm your AWS CLI is on a version released after the July 2026 feature launch. Older CLI versions won't recognize the parameter.
- Provisioned concurrency stuck "IN_PROGRESS": This is normal for the first few minutes after you request it, since AWS is pre-warming environments. Check status with
aws lambda get-provisioned-concurrency-config. - Bedrock InvokeModel returns AccessDeniedException: Your execution role needs an explicit
bedrock:InvokeModelstatement. It isn't included in any of the managed policies attached in Step 2. - EventBridge rule never fires: Double check the cron syntax uses six fields (Lambda's EventBridge cron requires a day-of-week field) and that the Lambda permission statement ID matches the rule's source ARN exactly.
- High cold-start latency even with provisioned concurrency: Confirm you're invoking the versioned or aliased ARN (the qualifier from Step 9), not
$LATEST. Provisioned concurrency only applies to the specific version or alias it was configured against.
Advanced Tips for Production-Ready Lambda Apps
Once the tutorial version is working, a few adjustments separate a demo from something you'd actually run in production. Move from a single monolithic zip to Lambda Layers for shared dependencies like the AWS SDK modules, which keeps your function-specific package small and deploys faster. Set up a CI/CD pipeline (GitHub Actions or AWS CodePipeline both work) that runs sam build and sam deploy automatically on merge, rather than hand-running the CLI commands from this tutorial every time.
For the DynamoDB layer, add a Global Secondary Index once you need to query tasks by something other than taskId, like status or due date, since a full table scan doesn't hold up past a few hundred records. On the Bedrock side, if you're building the durable-function pattern from Step 11 for real production traffic, look at AWS Step Functions Express Workflows as the orchestration layer sitting above Lambda. It's built specifically for the checkpoint-and-resume behavior that multi-call AI pipelines need, and it integrates natively with the same Lambda functions you already wrote here.
Finally, budget for observability from day one. CloudWatch Logs Insights queries let you search structured logs across every invocation without exporting anything, and AWS X-Ray tracing (a checkbox on create-function) shows you exactly where time is spent across API Gateway, Lambda, DynamoDB, and Bedrock calls in a single request trace. Both cost very little relative to what they save you the first time something breaks in production at 2 a.m.
Frequently Asked Questions
Is AWS Lambda free to use for this tutorial?
Yes. The AWS free tier includes 1 million free Lambda requests per month and 400,000 GB-seconds of compute time, which comfortably covers everything built here. DynamoDB's pay-per-request mode also falls within free-tier limits for this scale of testing.
What's the difference between S3ObjectStorageMode COPY and REFERENCE?
COPY is the default: Lambda duplicates your deployment package into its own internal storage, counting against your account's code storage quota. REFERENCE, introduced in July 2026, has Lambda read the code directly from your S3 bucket at invocation time instead, avoiding the duplication.
Do I need Lambda durable functions for a simple API?
No. Durable functions matter for multi-step workflows that need to survive retries or timeouts, like the Bedrock summarization pattern in Step 11. A basic CRUD API like the one in Steps 6-8 doesn't need them.
Why use API Gateway instead of a Lambda Function URL?
Function URLs are simpler for a single endpoint, but API Gateway gives you routing across multiple paths and methods, request validation, throttling, and custom domains, which this multi-route task API needs.
How much does provisioned concurrency actually cost per month?
At roughly $0.015 per GB-hour, two provisioned instances at 256 MB each running 24/7 comes out to a modest monthly cost, well under $10. Scale that estimate up based on your memory allocation and instance count before committing in production.
Can I use Python instead of Node.js for this tutorial?
Yes. Lambda supports Python, Java, Go, Ruby, and .NET as managed runtimes alongside Node.js. The IAM, API Gateway, EventBridge, and DynamoDB steps are identical regardless of runtime. Only the handler code syntax changes.
What happens if my deployment package exceeds 250 MB unzipped?
Lambda rejects the deployment. Split shared dependencies into a Lambda Layer, trim unused packages, or move large static assets (like ML model weights) to S3 and load them at runtime instead of bundling them into the package.
Is the 1,000 concurrent execution limit per function or per account?
It's a regional account-level default, shared across all functions in that region unless you configure reserved concurrency per function. Request an increase through AWS Support if your traffic needs more headroom.
Does this tutorial's architecture work outside us-east-1?
Yes, every service used here (Lambda, API Gateway, DynamoDB, EventBridge, Bedrock, CloudWatch) is available in all standard AWS regions. Swap us-east-1 for your preferred region in every command, but keep the region consistent across all resources since cross-region references between Lambda and DynamoDB or S3 add latency and complexity you don't need for this build.
How do I tear down everything from this tutorial when I'm done?
Delete resources in reverse order of creation: the CloudWatch alarm, the EventBridge rule and target, the API Gateway API, the Lambda function, the DynamoDB table, the IAM role and policy, and finally the S3 bucket. Leaving the DynamoDB table and provisioned concurrency running is what quietly generates the most unexpected charges, so prioritize deleting those two first if you're short on time.
Related Coverage
- Cloudflare Workers Setup: 12 Steps, 30 Min [2026]
- AWS vs GCP vs Azure: GCP Cuts SQL Costs 30% [2026]
- AWS Outage Hits 28 Hours, Third us-east-1 Failure [2026]
- Google Cloud Hits 63% Growth, Outpaces AWS, Azure [2026]
- Kubernetes Ingress-Nginx Flaw: CVSS 8.8, Still Unpatched [2026]
- More Cloud Computing coverage




