AWS Lambda now runs trillions of invocations a month across millions of AWS accounts, and it remains the fastest on-ramp into serverless computing for developers who don’t want to manage a single server. If you’ve typed “aws lambda tutorial” into a search bar this week, you’re one of roughly 12,100 people a month doing the same thing in the US alone, based on current keyword tracking data. This guide walks through building a real serverless application from scratch: a Lambda function wired to Amazon API Gateway, triggered by an S3 upload, storing results in DynamoDB, monitored through CloudWatch, and deployed with the AWS SAM CLI. By the end you’ll have a working REST API running on infrastructure you never had to patch, reboot, or scale manually.
This is a hands-on build, not a conceptual overview. You’ll create an AWS account (or use an existing one), install the tools, write real handler code in Python, deploy it, break it on purpose to learn how errors surface, and fix it. The whole thing runs inside the AWS Free Tier for testing purposes, so you can follow along without a credit card bill you didn’t expect.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Is AWS Lambda and Why It Matters in 2026
AWS Lambda is a compute service that runs your code in response to events without requiring you to provision or manage servers. You upload a function, attach it to a trigger (an HTTP request, a file upload, a database change, a scheduled timer), and AWS handles the provisioning, scaling, patching, and teardown of the underlying compute. You pay only for the milliseconds your code actually executes, not for idle capacity sitting around waiting for traffic.
The pitch hasn’t changed much since Lambda launched in 2014, but the runtime landscape has moved fast. According to AWS’s current documentation, the console now defaults new function creation to modern managed runtimes, and AWS has been steadily retiring older language versions on a rolling deprecation schedule tied to each language’s own end-of-life date. If you’re following an older tutorial that references Node.js 12 or Python 3.7, that guidance is stale — those runtimes are long past AWS’s deprecation cutoff and functions created with them will fail to deploy in the console today.
What makes Lambda worth learning specifically in 2026 is how far the ecosystem around it has matured. API Gateway, EventBridge, Step Functions, and the SAM CLI now form a genuinely productive serverless stack, and AWS’s own workshop and getting-started documentation was refreshed as recently as this month, according to AWS’s public documentation timestamps. That’s a signal AWS is still actively pushing Lambda as the default entry point for new serverless workloads, not a legacy service being left to rot.
Prerequisites: What You Need Before You Start
Gather these before Step 1. Skipping this section is the single biggest reason people get stuck halfway through a Lambda tutorial and give up.
| Requirement | Version / Detail | Notes |
|---|---|---|
| AWS account | Active, billing enabled | Free Tier covers everything in this tutorial for a new or low-usage account |
| AWS CLI | v2, latest release | Required for SAM deploys and scripted testing |
| AWS SAM CLI | Latest release | Used to build, package, and deploy the serverless stack locally |
| Python | 3.13 or 3.12 | Matches Lambda’s current supported managed runtimes; avoid anything below 3.11 |
| Node.js | 22.x (optional) | Only needed if you follow the Node.js code samples instead of Python |
| Docker | Desktop, latest | Needed for `sam local invoke` to emulate the Lambda execution environment |
| IAM permissions | Programmatic access | An IAM user or role with Lambda, API Gateway, S3, DynamoDB, and CloudWatch permissions |
| Code editor | VS Code or similar | AWS Toolkit extension is optional but speeds up local debugging |
You do not need prior serverless experience, but basic familiarity with the command line and a scripting language (Python or JavaScript) will make this go faster. Budget 90 minutes for a first pass through every step, including the AWS console back-and-forth that always eats more time than you plan for.
Step 1: Create Your AWS Account and Root User Safeguards
If you already have an AWS account, skip to Step 2. If not, sign up at the AWS console sign-up flow and enable billing. The moment your account is live, do two things before anything else: enable MFA on the root user, and create an IAM user with programmatic access instead of using root credentials for daily work. AWS’s own security guidance is blunt about this — the root user should be used only for account-level tasks like changing your support plan, and never for day-to-day development.
Create an IAM user (or better, an IAM Identity Center permission set if your organization uses it) with the `AWSLambda_FullAccess`, `AmazonAPIGatewayAdministrator`, `AmazonS3FullAccess`, and `AmazonDynamoDBFullAccess` managed policies attached for this tutorial. In production you’d scope these down to least-privilege custom policies, but broad managed policies are fine for a learning environment you’ll tear down afterward.
Step 2: Install and Configure the AWS CLI
Install AWS CLI v2 for your OS, then configure it with the access key and secret from the IAM user you created in Step 1.
# macOS
brew install awscli
# Windows (PowerShell, run as admin)
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
# Linux
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure credentials
aws configure
# AWS Access Key ID: [paste your key]
# AWS Secret Access Key: [paste your secret]
# Default region name: us-east-1
# Default output format: json
# Verify
aws sts get-caller-identity
The `get-caller-identity` call should return your account number, user ARN, and user ID. If it errors out, your credentials weren’t saved correctly — re-run `aws configure` and double-check you copied the secret key without a trailing space.
Step 3: Install the AWS SAM CLI and Docker
The Serverless Application Model (SAM) CLI is AWS’s own framework for building, testing, and deploying Lambda applications as infrastructure-as-code, rather than clicking through the console every time you change a line of code. It also lets you invoke functions locally inside a Docker container that mirrors the real Lambda execution environment, which is the fastest way to catch bugs before they cost you a deploy cycle.
# macOS
brew tap aws/tap
brew install aws-sam-cli
# Windows: download the SAM CLI MSI installer from AWS docs and run it
# Linux
wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install
# Verify
sam --version
docker --version
Make sure Docker Desktop is actually running before you try `sam local invoke` later — this is the number one cause of confusing SAM errors that have nothing to do with your code.
Step 4: Create Your First Lambda Function From the Console
Before touching infrastructure-as-code, build one function by hand in the console so you understand what SAM is automating later. Open the Functions page of the Lambda console and choose Create function, then Author from scratch. AWS’s own getting-started documentation, updated as recently as this week, walks through exactly this flow.
Set the function name to `myFirstLambdaFunction`, choose Python 3.13 as the runtime, and leave the default execution role (AWS creates one automatically with basic CloudWatch Logs permissions). Click Create function. Replace the default handler code with this:
import json
def lambda_handler(event, context):
print("Received event:", json.dumps(event))
return {
"statusCode": 200,
"body": json.dumps({
"message": "Hello from Lambda!",
"requestId": context.aws_request_id
})
}
Click Deploy to save the code, then click Test, create a new test event using the default “hello-world” template, and run it. You should see a 200 response with the JSON body in the execution results panel, along with a duration in milliseconds and billed duration — the two numbers that determine your actual cost.
Step 5: Understand the Lambda Execution Model
Before building anything more complex, it’s worth understanding what just happened under the hood, because it explains almost every weird bug you’ll hit later. According to AWS’s serverless developer guide, processing an event with Lambda breaks down into three conceptual steps: you configure the entry point (the handler), the Lambda service initializes the function and its runtime environment, then it invokes the handler with an event object and a context object.
That “initializes the function” step is where cold starts come from. The first invocation after a period of inactivity has to spin up a fresh execution environment, load your code, and run any code outside the handler function before it can process the event — this is the cold start penalty you’ll see referenced constantly in serverless discussions. Subsequent invocations within a short window reuse that warm environment, which is why the second and third test runs in the console will almost always show a lower duration than the first.
| Concept | What It Means | Why It Matters |
|---|---|---|
| Handler | The function AWS calls when your Lambda is invoked | Must match the format `filename.function_name` in your config |
| Event object | JSON payload describing what triggered the invocation | Structure varies by trigger source (API Gateway, S3, DynamoDB, etc.) |
| Context object | Runtime metadata: request ID, memory limit, time remaining | Use `context.get_remaining_time_in_millis()` to avoid timeouts |
| Cold start | Latency from spinning up a fresh execution environment | Typically 100ms-1s depending on runtime and package size |
| Execution role | IAM role Lambda assumes to run your code | Controls what AWS resources your function can touch |
| Concurrency | Number of simultaneous executions | Default account limit is 1,000 unless you request an increase |
Step 6: Set Up Your Local SAM Project Structure
Now switch from console-driven development to infrastructure-as-code, which is how you’ll actually want to manage anything beyond a toy function. Initialize a new SAM project:
sam init --runtime python3.13 --name lambda-s3-api-tutorial --app-template hello-world
cd lambda-s3-api-tutorial
tree .
# lambda-s3-api-tutorial/
# ├── hello_world/
# │ ├── app.py
# │ └── requirements.txt
# ├── tests/
# ├── template.yaml
# ├── samconfig.toml
# └── README.md
The `template.yaml` file is the heart of a SAM project — it’s a CloudFormation template with SAM-specific shorthand that defines every resource in your stack: functions, API Gateway routes, S3 buckets, DynamoDB tables, IAM permissions, and how they connect. Everything you clicked through manually in Step 4 can be expressed here as declarative config that you can version-control, review, and redeploy identically every time.
Step 7: Build a REST API With API Gateway and DynamoDB
Now build something closer to a real application: a REST API backed by Lambda that writes and reads items from a DynamoDB table. AWS’s own tutorial for this exact pattern outlines four stages: create and configure a Lambda function to perform operations on a DynamoDB table, create a REST API in API Gateway to connect to that function, create the DynamoDB table and test it in the console, then deploy the API and test the full setup with curl. That’s the shape we’re following here, wired through SAM instead of manual console clicks.
Replace `template.yaml` with the following:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda + API Gateway + DynamoDB tutorial stack
Globals:
Function:
Timeout: 10
MemorySize: 256
Runtime: python3.13
Resources:
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: tutorial-items
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ItemsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: items_function/
Handler: app.lambda_handler
Environment:
Variables:
TABLE_NAME: !Ref ItemsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable
Events:
GetItems:
Type: Api
Properties:
Path: /items
Method: get
CreateItem:
Type: Api
Properties:
Path: /items
Method: post
Outputs:
ApiEndpoint:
Description: API Gateway endpoint URL
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/items"
Create `items_function/app.py` with the handler logic:
import json
import os
import uuid
import boto3
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
def lambda_handler(event, context):
method = event.get("httpMethod", "GET")
if method == "POST":
body = json.loads(event.get("body") or "{}")
item_id = str(uuid.uuid4())
table.put_item(Item={"id": item_id, "name": body.get("name", "unnamed")})
return {
"statusCode": 201,
"body": json.dumps({"id": item_id, "status": "created"})
}
response = table.scan()
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(response.get("Items", []))
}
Add `boto3` to `items_function/requirements.txt` (it’s included in the Lambda Python runtime by default, but pinning it locally keeps `sam build` consistent). This function branches on HTTP method: a POST creates an item with a generated UUID, a GET scans and returns every item in the table.
Step 8: Build, Test Locally, and Deploy the Stack
Build the SAM application, which resolves dependencies and packages your code:
sam build
# Test locally with a simulated API Gateway event, no AWS resources needed
sam local invoke ItemsFunction --event events/get-event.json
# Or spin up a local API Gateway emulator on port 3000
sam local start-api
curl http://127.0.0.1:3000/items
Once local testing looks good, deploy to AWS with guided mode, which walks you through stack naming, region confirmation, and IAM capability acknowledgment:
sam deploy --guided
# Answer the prompts:
# Stack Name: lambda-s3-api-tutorial
# AWS Region: us-east-1
# Confirm changes before deploy: Y
# Allow SAM CLI IAM role creation: Y
# Save arguments to configuration file: Y
# Subsequent deploys are simpler
sam deploy
When the deploy finishes, SAM prints the `ApiEndpoint` output from your template. Test it with curl exactly like AWS’s own tutorial recommends:
curl -X POST https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/items \
-H "Content-Type: application/json" \
-d '{"name": "first tutorial item"}'
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/items
The first call returns a 201 with a generated ID. The second call returns the full array of items currently sitting in your DynamoDB table, proving the whole chain — API Gateway, Lambda, DynamoDB — is wired correctly end to end.
Step 9: Add an S3 Trigger for Event-Driven Processing
REST APIs are one trigger pattern; event-driven file processing is another common one. AWS’s getting-started guidance specifically calls out setting up an S3 event trigger so a Lambda function fires automatically when an object lands in a bucket — this is the pattern behind thumbnail generation, log processing, and document ingestion pipelines. Add this to your `template.yaml`:
UploadsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "tutorial-uploads-${AWS::AccountId}"
ProcessUploadFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: process_upload/
Handler: app.lambda_handler
Policies:
- S3ReadPolicy:
BucketName: !Sub "tutorial-uploads-${AWS::AccountId}"
Events:
S3Upload:
Type: S3
Properties:
Bucket: !Ref UploadsBucket
Events: s3:ObjectCreated:*
# process_upload/app.py
import json
import urllib.parse
def lambda_handler(event, context):
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
size = record["s3"]["object"]["size"]
print(f"New object: s3://{bucket}/{key} ({size} bytes)")
return {"statusCode": 200, "body": json.dumps({"processed": len(event["Records"])})}
Run `sam build && sam deploy`, then upload a test file to confirm the trigger fires:
echo "test upload" > test.txt
aws s3 cp test.txt s3://tutorial-uploads-YOUR_ACCOUNT_ID/test.txt
# Check logs
sam logs -n ProcessUploadFunction --stack-name lambda-s3-api-tutorial --tail
Step 10: Monitor With CloudWatch Logs and Metrics
Every Lambda invocation automatically streams logs to CloudWatch as long as your execution role includes basic logging permissions (SAM adds this by default). You’ll live in CloudWatch Logs constantly once your functions are past the toy-example stage, so learn the key commands now instead of clicking through the console every time.
# Tail logs for a specific function in real time
sam logs -n ItemsFunction --stack-name lambda-s3-api-tutorial --tail
# Filter for errors only
aws logs filter-log-events \
--log-group-name /aws/lambda/lambda-s3-api-tutorial-ItemsFunction \
--filter-pattern "ERROR"
# Check invocation, error, and duration metrics
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Errors \
--dimensions Name=FunctionName,Value=lambda-s3-api-tutorial-ItemsFunction \
--start-time 2026-08-17T00:00:00Z \
--end-time 2026-08-18T00:00:00Z \
--period 3600 \
--statistics Sum
Set a CloudWatch alarm on the Errors metric for anything you deploy beyond a personal test project. A function that starts silently failing at 2 a.m. because of an expired API key somewhere downstream is a much worse discovery at 9 a.m. the next day than an alert the moment it happened.
Step 11: Understand Lambda Pricing and the Free Tier
Lambda billing is based on two dimensions: the number of requests and the compute time your function consumes, measured in GB-seconds (memory allocated multiplied by execution duration). AWS’s Free Tier includes a fixed monthly allowance of requests and compute time at no charge, which is generous enough that a personal project or this entire tutorial will realistically cost nothing.
| Pricing Dimension | Detail | Practical Impact |
|---|---|---|
| Requests | Billed per million requests | Free Tier covers a substantial monthly allowance before charges begin |
| Duration | Billed per GB-second, rounded to the nearest 1ms | Lower memory + faster code = lower bill, but more memory can sometimes finish faster and cost less overall |
| Memory range | 128 MB to 10,240 MB configurable | CPU scales proportionally with memory allocation |
| Max timeout | 900 seconds (15 minutes) | Long-running batch jobs may need Step Functions or Fargate instead |
| API Gateway REST API | Billed per million API calls plus data transfer | Separate from Lambda’s own billing, adds up on high-traffic APIs |
| DynamoDB on-demand | Billed per read/write request unit | Pay-per-request billing mode used in this tutorial avoids capacity planning |
The classic beginner mistake is over-allocating memory “just in case.” Start at 256 MB for lightweight API handlers and only raise it if CloudWatch metrics show your function is CPU-starved or hitting timeout errors. For CPU-bound workloads like image processing, more memory often reduces total cost even though the per-GB-second rate is technically higher, because the function finishes so much faster.
Step 12: Secure Your Function With Least-Privilege IAM
The `DynamoDBCrudPolicy` and `S3ReadPolicy` shortcuts used earlier in this tutorial are SAM policy templates — they generate scoped IAM policies automatically instead of forcing you to hand-write JSON. That’s the right default for learning, but before anything touches production, audit exactly what each function’s execution role can do.
# List the execution role attached to a function
aws lambda get-function --function-name lambda-s3-api-tutorial-ItemsFunction \
--query 'Configuration.Role'
# List policies attached to that role
aws iam list-attached-role-policies --role-name YOUR_ROLE_NAME
aws iam list-role-policies --role-name YOUR_ROLE_NAME
Never attach `AdministratorAccess` or overly broad wildcard policies to a Lambda execution role, even temporarily “to test something.” A compromised function with admin rights is a full account compromise. Scope every role to the specific table, bucket, or queue it actually needs, and use resource-level ARNs instead of `*` wherever the service supports it.
What AWS’s Own Documentation Actually Says
It’s worth reading the source material directly rather than relying on secondhand summaries, since AWS updates these pages frequently and third-party tutorials go stale fast. Here’s what AWS’s current documentation says, word for word, about the exact workflow covered in this guide.
On creating your first function, AWS’s Lambda developer guide states: “To create a Hello world Lambda function with the console: Open the Functions page of the Lambda console. Choose Create function. Select Author from scratch.” That’s the exact three-click path this tutorial’s Step 4 walks through.
On the four-stage build used in Step 7, AWS’s API Gateway integration tutorial describes the flow this way: “To complete this tutorial, you go through the following stages: 1. Create and configure a Lambda function in Python or Node.js to perform operations on a DynamoDB table. 2. Create a REST API in API Gateway to connect to your Lambda function. 3. Create a DynamoDB table and test it with your Lambda function in the console. 4. Deploy your API and test the full setup using curl in a terminal.”
On wiring an S3 trigger, AWS’s own Lambda getting-started page instructs: “Finally, set up an event trigger for Amazon S3 that will invoke your Lambda function when an event occurs.” That’s precisely the `s3:ObjectCreated:*` event source configured in Step 9’s `template.yaml`.
On locating the console entry point, AWS’s serverless code tutorial notes: “In the top navigation bar, search for Lambda and open the AWS Lambda Console” — a small detail, but the exact navigation path AWS expects a first-time user to follow.
And for teams who reach for the Serverless Framework instead of SAM, its documentation explains the equivalent config structure: “All of the Lambda functions in your serverless service can be found in serverless.yml under the functions property.” Functionally this plays the same role as the `Resources` block in this tutorial’s `template.yaml` — different tooling, same underlying CloudFormation-driven deployment model.
Real-World Use Cases Beyond This Tutorial
The REST API and S3 trigger patterns covered here are the two most common Lambda entry points, but they’re far from the only ones. Once you’re comfortable with the basic build, these are the patterns worth exploring next.
- Scheduled jobs. An EventBridge Scheduler rule can invoke a Lambda function on a cron-style schedule — nightly reports, cleanup jobs, or periodic health checks without a dedicated server running 24/7.
- Stream processing. Lambda can consume records from Kinesis Data Streams or a DynamoDB Stream in near real time, useful for fraud detection, activity feeds, or change-data-capture pipelines.
- Chatbot and voice backends. Lex and Alexa Skills both use Lambda as their fulfillment layer, translating parsed user intent into business logic.
- Image and video processing. The S3 trigger pattern from Step 9 is the backbone of most thumbnail generation, format conversion, and content moderation pipelines built on AWS.
- Authentication hooks. Amazon Cognito supports Lambda triggers at nearly every stage of the sign-up and sign-in flow, letting you customize validation, enrichment, and post-confirmation logic.
- CI/CD automation. Lambda functions triggered by CodePipeline or GitHub webhooks can run custom validation, notify a Slack channel, or kick off downstream deployments.
Testing Your Lambda Functions With pytest
Console testing and `sam local invoke` cover manual checks, but any function you intend to maintain needs real unit tests that run in CI before every deploy. Since the handler in Step 7 talks to DynamoDB through `boto3`, mock that client rather than hitting a real table during tests.
# tests/unit/test_items_function.py
import json
import os
import boto3
from moto import mock_aws
import pytest
os.environ["TABLE_NAME"] = "tutorial-items-test"
@mock_aws
def test_post_creates_item():
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
dynamodb.create_table(
TableName="tutorial-items-test",
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
BillingMode="PAY_PER_REQUEST",
)
from items_function import app
event = {"httpMethod": "POST", "body": json.dumps({"name": "unit test item"})}
response = app.lambda_handler(event, None)
assert response["statusCode"] == 201
body = json.loads(response["body"])
assert body["status"] == "created"
@mock_aws
def test_get_returns_empty_list_when_table_empty():
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
dynamodb.create_table(
TableName="tutorial-items-test",
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
BillingMode="PAY_PER_REQUEST",
)
from items_function import app
response = app.lambda_handler({"httpMethod": "GET"}, None)
assert response["statusCode"] == 200
assert json.loads(response["body"]) == []
Install the test dependencies and run the suite before every `sam deploy`:
pip install pytest moto boto3
python -m pytest tests/unit -v
Wiring this into a GitHub Actions workflow that runs `pytest` on every pull request, then only allows `sam deploy` to run on merges to `main`, is the difference between a personal experiment and a function a team can trust in production.
Common Pitfalls When Learning AWS Lambda
These are the mistakes that trip up almost everyone on their first serverless project, based on patterns that show up constantly in AWS’s own troubleshooting documentation and developer forums.
- Forgetting the handler path format. The handler must be `filename.function_name`, not just the function name. If your file is `app.py` and your function is `lambda_handler`, the handler value is `app.lambda_handler` — get this wrong and every invocation fails with “Unable to import module.”
- Deploying without running `sam build` first. SAM caches build artifacts separately from your source directory. Editing code and running `sam deploy` without `sam build` in between deploys the old, unbuilt code.
- Setting the timeout too low. The default 3-second timeout is often too short for anything touching a database or external API on a cold start. Bump it to 10-30 seconds during development, then tune down once you understand your real latency profile.
- Hardcoding credentials in function code. Never embed AWS keys or database passwords directly in a Lambda handler. Use environment variables for config and Secrets Manager or Parameter Store for anything sensitive.
- Ignoring cold starts in latency-sensitive APIs. If your API needs consistent sub-100ms response times, plain Lambda without provisioned concurrency probably isn’t the right fit — the first request after idle time will always pay the cold start tax.
- Overly broad IAM policies “to make it work.” Attaching `*` resource permissions to unblock a deploy and forgetting to scope it down afterward is one of the most common serverless security gaps found in cloud audits.
- Not setting a Dead Letter Queue or destination for async invocations. When a Lambda triggered by S3 or SNS fails, the event can silently disappear unless you configure an on-failure destination to capture it.
- Testing only in the console, never locally. Skipping `sam local invoke` means every debug cycle costs a full deploy, which is slow and makes iteration painful.
Expected Output at Each Stage
Here’s what success looks like at the key checkpoints, so you know whether to keep going or stop and debug.
# After Step 4 (console test)
{
"statusCode": 200,
"body": "{\"message\": \"Hello from Lambda!\", \"requestId\": \"a1b2c3d4-...\"}"
}
# Duration: 12.4 ms Billed Duration: 13 ms Memory Used: 42 MB
# After Step 8 (sam deploy output)
CloudFormation outputs from deployed stack
-------------------------------------------------------------------
Outputs
-------------------------------------------------------------------
Key ApiEndpoint
Description API Gateway endpoint URL
Value https://abc123xyz.execute-api.us-east-1.amazonaws.com/Prod/items
-------------------------------------------------------------------
# After Step 9 (S3 trigger test)
START RequestId: 5f6e7d8c-... Version: $LATEST
New object: s3://tutorial-uploads-123456789012/test.txt (13 bytes)
END RequestId: 5f6e7d8c-...
REPORT RequestId: 5f6e7d8c-... Duration: 45.20 ms Billed Duration: 46 ms
Troubleshooting Guide: Fixing the Errors You’ll Actually Hit
| Error | Likely Cause | Fix |
|---|---|---|
| “Unable to import module ‘app’: No module named ‘boto3′” | Missing dependency not bundled in deployment package | Add the package to `requirements.txt` and re-run `sam build` |
| Task timed out after 3.00 seconds | Default timeout too short for the operation | Increase `Timeout` in `template.yaml` under Globals or per-function |
| AccessDeniedException on DynamoDB call | Execution role missing table permissions | Add or verify the `DynamoDBCrudPolicy` SAM policy template targets the right table |
| {“message”: “Internal server error”} from API Gateway | Lambda returned a malformed response (missing `statusCode` or `body`) | Ensure every code path returns a dict with `statusCode` and `body` keys |
| sam build fails with “Docker is not reachable” | Docker Desktop isn’t running | Start Docker Desktop before running `sam build` or `sam local invoke` |
| CORS errors calling the API from a browser | API Gateway not configured to return CORS headers | Add `Cors` config under the `Api` event source in `template.yaml` |
| Function works locally but fails when deployed | Environment variable or IAM permission difference between local and cloud | Diff your local `.env` against the deployed function’s environment variables in the console |
| Deploy hangs on “Waiting for changeset to be created” | No actual changes detected, or a stuck CloudFormation stack | Check the CloudFormation console for a stack in `UPDATE_ROLLBACK_FAILED` state and resolve it before retrying |
| Throttling: Rate exceeded | Concurrent execution limit reached | Request a service quota increase or add retry logic with exponential backoff |
Advanced Tips for Production Lambda Workloads
Once the basic build works, these are the moves that separate a tutorial project from something you’d actually run in production.
- Use Lambda Powertools for structured logging, tracing, and metrics instead of raw `print()` statements — it standardizes observability across every function in a team codebase.
- Enable X-Ray tracing on functions that call multiple downstream services, so you can see exactly where time is spent across a Lambda-to-DynamoDB-to-S3 chain instead of guessing.
- Use provisioned concurrency for latency-sensitive endpoints where cold starts are unacceptable, but only on the specific functions that need it — it’s billed continuously, unlike on-demand Lambda.
- Split large functions by responsibility rather than building one giant handler with a dozen `if` branches. Smaller functions deploy faster, have smaller blast radii, and are easier to reason about in CloudWatch.
- Version and alias your functions so you can shift traffic gradually between a new deploy and the previous stable version instead of an all-at-once cutover.
- Set reserved concurrency on functions that could otherwise starve the rest of your account’s concurrency pool during a traffic spike.
- Store secrets in AWS Secrets Manager and cache them in-memory across warm invocations rather than fetching them on every single call.
Lambda vs. Other AWS Compute Options
Lambda isn’t the right tool for every workload. Knowing when to reach for something else saves you from fighting the platform’s limits.
| Service | Best For | Limitation vs. Lambda |
|---|---|---|
| AWS Lambda | Short, event-driven, bursty workloads | 15-minute max execution time, cold starts |
| AWS Fargate | Long-running containers, steady traffic | No true scale-to-zero, higher baseline cost for low traffic |
| EC2 | Full OS control, specialized hardware, GPUs | You manage patching, scaling, and capacity planning |
| AWS Step Functions + Lambda | Long, multi-step workflows chaining several functions | Adds orchestration complexity and its own billing dimension |
Cleaning Up to Avoid Ongoing Charges
When you’re done experimenting, tear down the stack so nothing keeps running unnecessarily. Empty the S3 bucket first, since CloudFormation won’t delete a non-empty bucket automatically.
# Empty the S3 bucket
aws s3 rm s3://tutorial-uploads-YOUR_ACCOUNT_ID --recursive
# Delete the entire stack: functions, API Gateway, DynamoDB table, IAM roles
aws cloudformation delete-stack --stack-name lambda-s3-api-tutorial
# Confirm deletion
aws cloudformation describe-stacks --stack-name lambda-s3-api-tutorial
That last command should eventually return a “stack does not exist” error, which confirms cleanup finished successfully.
Frequently Asked Questions
Is AWS Lambda free to use?
The AWS Free Tier includes a monthly allowance of requests and compute time at no cost, and this tutorial’s usage stays well within that allowance for a new or lightly used account. Beyond the Free Tier, billing is based on requests and GB-seconds of compute.
Which programming languages does Lambda support?
Lambda offers managed runtimes for Python, Node.js, Java, Go, .NET, and Ruby, plus a custom runtime option for any language via the Lambda Runtime API or container images.
What’s the maximum time a Lambda function can run?
15 minutes (900 seconds) per invocation. Workloads that regularly need longer than that should look at Step Functions for orchestration or Fargate for long-running containers.
Do I need Docker to use Lambda?
Not for deployment — Docker is only required locally if you want to use `sam local invoke` or `sam local start-api` to test functions on your machine before deploying, or if you choose to package your function as a container image instead of a ZIP archive.
What causes a Lambda cold start?
A cold start happens when AWS has to provision a brand-new execution environment for your function because no warm one is available — either it’s the first invocation ever, or enough idle time has passed that the previous environment was recycled.
Can Lambda functions talk to a VPC-based database like RDS?
Yes, by attaching the function to a VPC with the correct subnet and security group configuration. This adds a small amount of extra cold start latency due to elastic network interface setup, though AWS has significantly reduced that overhead compared to Lambda’s earlier VPC networking model.
Is SAM the only way to deploy Lambda functions?
No. You can deploy manually through the console, use raw CloudFormation, use Terraform, use the Serverless Framework, or use the AWS CDK. SAM is AWS’s own opinionated shorthand for CloudFormation aimed specifically at serverless applications, which makes it a good starting point for learning.
How much memory should I allocate to a Lambda function?
Start at 256 MB for lightweight API handlers and adjust based on CloudWatch metrics. AWS Lambda Power Tuning, an open-source tool built on Step Functions, can automatically benchmark your function across memory levels to find the cost-optimal setting.


