Event-Driven Serverless with AWS Lambda
Build a serverless image pipeline triggered by S3 uploads and API Gateway.
Introduction
30 Second Summary
Think about how your phone automatically backs up photos the moment you take them. That kind of instant, event-driven reaction is exactly how modern cloud applications work at scale.
In this project, you will build a serverless pipeline on AWS where uploading a file to S3 automatically triggers a Lambda function. You will also connect that function to a public API, experience a classic VPC networking trap, and set up failure handling with a Dead Letter Queue.
What You'll Build
Upload a file and watch your Lambda function fire within seconds. Then paste a public URL into your browser and see the same function respond to an API request.
By the end of this project, you'll have:
- Upload any file to S3 and watch your Lambda function fire automatically within seconds, processing the event without any server to manage.
- Paste a public URL into your browser and see your Lambda function respond through a REST API endpoint powered by API Gateway.
- Experience the VPC networking trap firsthand where your function times out after losing internet access, then implement the NAT Gateway fix that restores connectivity.
- Configure concurrency controls and a Dead Letter Queue that catches failed invocations instead of letting them vanish silently.
- Secret Mission: Build a Step Functions state machine that orchestrates multiple Lambda functions with conditional branching in a visual workflow.
Are there any prerequisites?
This project is part of a series. You need an AWS account with console access and a basic understanding of IAM roles and VPC networking from Projects 1-3.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. You're about to create an event-driven serverless pipeline that covers the most heavily tested Lambda topics on the SAA-C03 exam.
Create and Test Your First Lambda Function
Before you can wire up triggers and integrations, you need a working function. Lambda functions are stateless, event-driven compute units that scale automatically.
Creating one from scratch in the console teaches the execution model and lets you configure the two most-tested Lambda limits on the SAA-C03 exam: memory (128 MB to 10,240 MB, with CPU scaling proportionally) and timeout (up to 900 seconds / 15 minutes).
In this step, get ready to:
- Create a Python Lambda function with an auto-generated execution role.
- Configure memory and timeout settings that appear frequently on the SAA-C03 exam.
- Write handler code and verify it executes successfully.
Create a Python Lambda function
Every Lambda function needs a runtime (the language it executes in) and an execution role (the IAM role that grants it permissions to other AWS services).
- In the AWS Management Console, use the search bar at the top to search for Lambda and open the Lambda console.
- On the Functions page, choose Create function.
- Select Author from scratch.
- In the Basic information pane, for Function name, enter image-processor.
- For Runtime, choose Python 3.12.
- Leave Architecture set to x86_64.
- Under Permissions, leave the default option to create a new role with basic Lambda permissions.
- Choose Create function.
The console opens your new function's code editor. Lambda also created an execution role that grants basic permissions to write to CloudWatch Logs.
What is an execution role?
An execution role is an IAM role that defines what your Lambda function is allowed to do. The auto-created role includes the AWSLambdaBasicExecutionRole policy, which grants three permissions: CreateLogGroup, CreateLogStream, and PutLogEvents.
You will expand this role in later steps when the function needs to access S3 and other services.
Configure memory and timeout
Memory and timeout are the two Lambda settings tested most heavily on the SAA-C03 exam. Memory controls how much RAM your function gets, and CPU scales proportionally with it.
- Choose the Configuration tab.
- Choose General configuration.
- Choose Edit.
- For Memory, set the value to 256 MB.
- For Timeout, set the value to 0 min 30 sec (30 seconds total).
- Choose Save.
The General configuration panel now shows 256 MB memory and a 30-second timeout.
Key Lambda limits for the exam
- Memory: 128 MB to 10,240 MB in 1 MB increments. At 1,769 MB you get 1 full vCPU.
- Timeout: maximum 900 seconds (15 minutes). This is the most-tested Lambda limit.
- Deployment package: 50 MB zipped, 250 MB unzipped (including layers).
- /tmp storage: 512 MB to 10,240 MB.
- Concurrent executions: 1,000 per region by default (adjustable via Service Quotas).
Write and test the function code
The default code just returns a hello message. You will replace it with a handler that logs the incoming event, detects the source (S3 or API Gateway), and returns a structured JSON response.
- Choose the Code tab to return to the code editor.
- Select the lambda_function.py file in the file explorer if it is not already open.
- Delete all existing code and replace it with the following:
import json
def lambda_handler(event, context):
# Log the incoming event for debugging
print("Received event:", json.dumps(event))
# Determine the event source
if 'Records' in event and len(event['Records']) > 0:
if event['Records'][0].get('eventSource') == 'aws:s3':
source = 'S3'
detail = event['Records'][0]['s3']['object']['key']
else:
source = 'Unknown'
detail = 'Unrecognized event source'
elif 'httpMethod' in event:
source = 'API Gateway'
detail = event.get('path', '/')
else:
source = 'Direct invocation'
detail = 'Test event or manual trigger'
# Build and return the response
response = {
'statusCode': 200,
'body': json.dumps({
'message': 'Image processor executed successfully',
'source': source,
'detail': detail
})
}
print("Returning response:", json.dumps(response))
return response
What does this code do?
- The lambda_handler function is the entry point Lambda calls on each invocation. It receives an event (JSON input) and a context object (runtime metadata).
- The source-detection block checks whether the event came from S3 (has Records with eventSource: aws:s3), API Gateway (has httpMethod), or a direct test invocation.
- The response returns statusCode: 200 with a JSON body. API Gateway requires this exact format to return a proper HTTP response.
✔️ Awesome, I've got everything!
Great. Make sure you have no red underlines or syntax warnings in the editor before deploying.
ⓧ I'd like to double check the full code
Your lambda_function.py should contain exactly the code block above. Scroll up and confirm the following are present, in order:
- The import json statement at the top.
- The lambda_handler function definition with event and context parameters.
- The source-detection if/elif/else block checking for S3 Records, httpMethod, and direct invocation.
- The response dictionary with statusCode: 200 and a JSON-serialized body.
- The final return response statement.
- In the toolbar above the code editor, choose Deploy.
A banner confirms the function was updated successfully. Now test it.
- Choose the Test tab.
- Under Test event, choose Create new event.
- For Event name, enter test-direct-invocation.
- Replace the default event JSON with the following:
{
"action": "process-image",
"filename": "test-photo.jpg"
}
- Choose Save.
Before you run this, what do you expect the function to return? Think about which branch of the source-detection logic this test event will hit.
- Choose Test.
The Execution result tab shows a green "Succeeded" status. Expand Details to see your function's JSON response showing "source": "Direct invocation" and "detail": "Test event or manual trigger".
The output also displays Duration (actual execution time) and Billed Duration (rounded up to the nearest millisecond). These are the metrics Lambda uses for billing.
Seeing an error instead of Succeeded?
- Check that you chose Deploy before running the test. Unsaved code changes do not take effect until deployed.
- If you see a syntax error, check for missing colons or incorrect indentation in your Python code. Python is whitespace-sensitive.
- Help me troubleshoot my Lambda function.
Your function is live and responding to events. Next up, you will wire an S3 bucket to trigger this function automatically whenever a file is uploaded.
Wire Up an S3 Trigger
Your function works when you manually press Test, but real serverless architectures react to events automatically. The most classic Lambda integration on the SAA-C03 exam is S3-to-Lambda: a file lands in a bucket and your function fires within seconds, with no server polling or cron job.
In this step, you will create an S3 bucket, wire it to your function as an event source, and update the function code to parse the S3 event payload. This teaches how push-based triggers work and how Lambda scales automatically (one concurrent invocation per event).
In this step, get ready to:
- Create an S3 bucket and attach it as a trigger to your Lambda function.
- Update the function code to extract the uploaded file's bucket name and key from the S3 event.
- Upload a test file and confirm the function fires automatically via CloudWatch Logs.
Create an S3 bucket and add the trigger
Before Lambda can react to file uploads, it needs a bucket to watch. You will create a new S3 bucket and then add it as a trigger directly from the Lambda console.
- Open the S3 console by typing S3 in the top search bar and selecting the service.
- Click Create bucket.
- For Bucket name, enter a globally unique name such as lambda-trigger-yourname-2026.
- Leave all other settings as defaults and click Create bucket.
Why does the bucket name need to be unique?
S3 bucket names are globally unique across all AWS accounts. Adding your name and the year prevents collisions with other learners.
Now return to Lambda and wire the bucket as a trigger.
- Navigate back to your image-processor function in the Lambda console.
- In the Function overview pane at the top, click Add trigger.
- Select S3 from the trigger source dropdown.
- Under Bucket, select the bucket you just created.
- Under Event types, confirm that All object create events is selected.
- Under Recursive invocation, select the check box to acknowledge that using the same S3 bucket for input and output is not recommended.
- Click Add.
What just happened behind the scenes?
When you add the trigger through the console, Lambda automatically does two things. It creates an event notification on the S3 bucket so the bucket knows to send events to your function.
It also adds a resource-based policy to your function allowing the s3.amazonaws.com service principal to invoke it. This is the "who can invoke Lambda" side of security (resource policy), as opposed to the execution role which controls "what Lambda can do."
Seeing "Unable to validate the following destination configurations"?
This error means the bucket already has an event notification pointing to a Lambda function that no longer exists or has changed permissions. Remove the stale notification from the bucket's Properties tab under Event notifications, then try adding the trigger again.
help me fix the S3 trigger destination configuration error
Update the function to process S3 events
Your trigger is wired, but the function code still uses the generic handler from Step 1. You need to update it to extract the bucket name and object key from the S3 event payload.
- In the Lambda console, click the Code tab to open the inline editor.
- Select all the existing code and replace it with the following:
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# Extract S3 event details from the first record
s3_record = event['Records'][0]['s3']
bucket_name = s3_record['bucket']['name']
object_key = s3_record['object']['key']
# Log the file details
logger.info(f"File uploaded: {object_key} in bucket: {bucket_name}")
# Return success response
return {
'statusCode': 200,
'body': json.dumps({
'message': 'File processed successfully',
'bucket': bucket_name,
'key': object_key
})
}
What does this code do?
- event['Records'][0]['s3'] digs into the S3 event payload. S3 sends a Records array with one entry per object that triggered the notification.
- s3_record['bucket']['name'] gives you the bucket that received the upload.
- s3_record['object']['key'] gives you the file path of the uploaded object.
- logger.info(...) writes the details to CloudWatch Logs so you can verify the trigger fired.
- Click Deploy to save and publish the updated code.
You should see a green "Successfully updated" banner at the top of the editor.
Don't see the Deploy button?
Make sure you are on the Code tab and have actually edited the code. The Deploy button only appears when there are unsaved changes.
help me deploy my updated Lambda function code
Test the S3 trigger end-to-end
The moment of truth. You will upload a file to S3 and then check whether Lambda picked it up automatically.
- Open the S3 console and click into your bucket.
- Click Upload.
- Click Add files and choose any small file from your computer (a text file or an image works fine).
- Click Upload to confirm.
Before you check the logs, predict: what do you expect to see in CloudWatch Logs when you open the latest log stream?
- Return to the image-processor function in the Lambda console.
- Click the Monitor tab.
- Click View CloudWatch Logs.
- Click the most recent log stream (the one at the top of the list).
You should see a log line containing your uploaded file's name, similar to: File uploaded: test-file.txt in bucket: lambda-trigger-yourname-2026.
What is a resource-based policy?
Lambda has two security boundaries. The execution role controls what your function can call out to (other AWS services). The resource-based policy controls who can invoke your function.
When you added the S3 trigger, the console automatically added a resource policy allowing s3.amazonaws.com to call your function. You can view this under Configuration > Permissions > Resource-based policy statements.
No new log stream appearing?
S3 event notifications typically arrive within seconds, but can occasionally take up to a minute. Wait 30 seconds and refresh the CloudWatch Logs page.
If no log appears after a minute, confirm the trigger is listed in the Function overview diagram under your image-processor function. Also verify that the S3 bucket region matches the Lambda function region.
help me troubleshoot my S3-to-Lambda trigger not firing
Your function now fires automatically whenever a file lands in S3. Next up, you will add a second trigger path so the same function can be invoked from a public URL via API Gateway.
Add an API Gateway REST API Trigger
Your Lambda function can already respond to S3 file uploads automatically. But what if a user or an external application needs to invoke the function directly over HTTP?
API Gateway is the AWS service that puts a public HTTP endpoint in front of your Lambda function. You will create a REST API because it supports the features most frequently tested on the SAA-C03 exam: API keys, per-client throttling, request validation, WAF integration, and caching.
In this step, get ready to:
- Create a REST API in API Gateway with a Regional endpoint.
- Add a /process resource with a GET method integrated with your Lambda function.
- Deploy the API and test it by hitting the public URL in your browser.
Create a REST API in API Gateway
API Gateway offers two types of HTTP API: REST API and HTTP API. REST APIs support richer features like API keys, per-client throttling, request validation, and WAF integration. HTTP APIs are cheaper but lack these features. For exam preparation, REST API is the one to learn.
- Navigate to the API Gateway console by typing API Gateway in the AWS console search bar and selecting it.
- Choose Create API.
- Find the REST API card (not the one labelled "HTTP API") and choose Build.
- For API name, enter image-processor-api.
- Keep API endpoint type set to Regional.
- Choose Create API.
You should see the Resources pane with only the root resource (/) listed.
Why REST API instead of HTTP API?
The SAA-C03 exam tests whether you know which API type supports features like API keys, per-client throttling, request validation, AWS WAF integration, and private API endpoints. REST APIs support all of these. HTTP APIs are designed with fewer features so they can be offered at a lower price.
When an exam question mentions throttling per client, WAF, or API keys, the answer is REST API.
Create a resource and method integrated with Lambda
Resources in API Gateway represent the URL paths your API exposes. You will create a /process resource and attach a GET method that forwards requests to your Lambda function using proxy integration.
- In the Resources pane, choose Create resource.
- Keep Proxy resource turned off.
- Keep Resource path as /.
- For Resource name, enter process.
- Keep CORS (Cross Origin Resource Sharing) turned off.
- Choose Create resource.
You should now see /process listed as a child resource under the root.
- Select the /process resource, then choose Create method.
- For Method type, select GET.
- For Integration type, select Lambda function.
- Turn on Lambda proxy integration.
- For Lambda function, select your region and enter image-processor, then select it from the dropdown.
- Choose Create method.
What is Lambda proxy integration?
With proxy integration turned on, API Gateway passes the entire incoming HTTP request (headers, query strings, body) directly to your Lambda function as the event object. Your function is responsible for returning a properly formatted response with a statusCode, headers, and body.
Without proxy integration, you would need to configure request/response mapping templates in API Gateway. Proxy integration is simpler and more common in real-world serverless applications.
What about the Lambda permission dialog?
When you create the method, API Gateway may show a dialog asking to add permission for API Gateway to invoke your Lambda function. Confirm it. This adds a resource-based policy to your function allowing apigateway.amazonaws.com to call it.
Deploy the API and test in your browser
An API in API Gateway is not callable until you deploy it to a stage. A stage is like a named snapshot of your API configuration that gets its own URL.
- In the Resources pane, choose Deploy API.
- For Stage, select New stage.
- For Stage name, enter prod.
- Choose Deploy.
You are taken to the Stages pane. Under Stage details, you can see the Invoke URL. It looks like:
https://{api-id}.execute-api.{region}.amazonaws.com/prod
- Copy the Invoke URL using the copy icon next to it.
- Open a new browser tab and paste the URL.
- Append /process to the end of the URL, so the full address looks like:
https://{api-id}.execute-api.{region}.amazonaws.com/prod/process
Before you press Enter, what do you think you will see? Will the browser show a JSON response from your Lambda function, or an error?
- Press Enter to send the request.
Your browser displays the JSON response from your Lambda function. This confirms that API Gateway is successfully forwarding HTTP requests to your Lambda function and returning the response.
Seeing a 500 Internal Server Error?
Your Lambda function must return a response in the format that proxy integration expects. It needs a statusCode field (integer) and a body field (string). If your function returns a plain string or a dict without statusCode, API Gateway returns a 500 error.
Check your Lambda function code to make sure the return statement includes both fields. Help me fix a 500 error from API Gateway proxy integration.
Where is the resource-based policy?
Your Lambda function now has two authorized invokers. To see them, go back to the Lambda console, open your image-processor function, and choose Configuration then Permissions. Under Resource-based policy statements, you will see entries for both S3 and API Gateway.
This is the resource policy concept tested on the exam. The execution role controls what Lambda can do (outbound permissions). The resource-based policy controls who can invoke Lambda (inbound permissions).
You now have two ways to trigger your Lambda function: an S3 upload and a public REST API endpoint. Next, you will attach the function to a VPC and discover why Lambda loses internet access when placed inside private subnets.
Attach Lambda to a VPC and Fix Internet Access
Many SAA-C03 exam questions test what happens when you attach a Lambda function to a VPC. By default, Lambda has internet access. The moment you attach it to a VPC, it loses that internet access and can only reach resources inside the VPC.
In this step, you will experience this trap firsthand. Your function will timeout trying to reach S3, and you will understand why a NAT Gateway in a public subnet is the required fix. You will also tighten your function's IAM permissions to follow least-privilege principles.
In this step, get ready to:
- Attach your Lambda function to the default VPC and observe it lose internet access.
- Understand the NAT Gateway requirement for internet-bound traffic from private subnets.
- Configure least-privilege IAM permissions scoped to your specific S3 bucket.
Update your function code to call the S3 API
Before attaching to a VPC, you need code that actually requires internet access. You will update your function to list objects in your S3 bucket using boto3. This way, after VPC attachment, you will see the function timeout when it can no longer reach the S3 API endpoint.
- Return to your image-processor function in the Lambda console.
- In the Code tab, select the code in lambda_function.py and replace it with the following:
import json
import boto3
def lambda_handler(event, context):
# Initialize the S3 client
s3 = boto3.client('s3')
# Determine the event source
if 'Records' in event:
# S3 trigger event
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
source = 'S3'
else:
# API Gateway or test event
bucket = 'lambda-trigger-yourname-2026'
key = 'N/A'
source = 'API Gateway'
What does the first half do?
- boto3.client('s3') creates an S3 client that needs internet access to reach the AWS S3 API endpoint.
- The if/else block detects whether the invocation came from an S3 event (with a Records key) or from API Gateway/test events.
- Add the second half of the handler directly below the code you just pasted:
# List objects in the bucket (requires internet)
try:
response = s3.list_objects_v2(
Bucket=bucket, MaxKeys=5
)
object_count = response.get('KeyCount', 0)
message = f'Listed {object_count} objects in {bucket}'
except Exception as e:
message = f'Error accessing S3: {str(e)}'
return {
'statusCode': 200,
'body': json.dumps({
'source': source,
'bucket': bucket,
'key': key,
'message': message
})
}
What does the second half do?
- list_objects_v2() calls the S3 API over the internet to list up to 5 objects in the bucket.
- The try/except block captures any failure (like a timeout) and returns it in the response body so you can see what went wrong.
- Click Deploy to save the updated code.
- Click Test to verify the function works before VPC attachment.
The execution should succeed and the response body should show a message listing objects in your bucket. This confirms the code works with internet access.
Seeing an AccessDenied error?
Your execution role does not yet have S3 permissions. This is expected at this stage. You will add the S3 permissions in the final substep. For now, confirm you see an error message in the response body rather than a timeout.
If you see a response with the error message quickly (within 1-2 seconds), that proves your function has internet access and can reach the S3 endpoint. Help me understand why my function is returning an access denied error.
Attach the function to the default VPC
Now you will attach the function to a VPC. This simulates a real-world scenario where Lambda needs to access VPC resources like databases or caches. The catch is that VPC-attached functions lose internet access unless you add a NAT Gateway.
- First, add the required VPC permissions to your execution role. Navigate to the IAM console by searching for IAM in the AWS search bar.
- Click Roles in the left sidebar.
- Search for and click your Lambda execution role (its name starts with image-processor-role).
- Click Add permissions, then Attach policies.
- Search for AWSLambdaVPCAccessExecutionRole and select it.
- Click Add permissions.
What is AWSLambdaVPCAccessExecutionRole?
This managed policy grants Lambda permission to create and manage Hyperplane Elastic Network Interfaces (ENIs). Lambda needs ENIs to connect your function to the VPC's subnets. Without this policy, the VPC attachment will fail.
- Return to the Lambda console and open your image-processor function.
- Choose the Configuration tab, then choose VPC.
- Choose Edit.
- Under VPC, select your default VPC from the dropdown.
- Under Subnets, select 2 private subnets. Look for subnets whose route tables do NOT have a route to an Internet Gateway.
- Under Security groups, select the default security group.
- Choose Save.
What happens during save?
Lambda creates Hyperplane ENIs in your selected subnets. This takes 30-60 seconds. During this time you cannot modify the function, but it can still be invoked. Each ENI supports up to 65,000 connections.
Wait until the function status returns to Active (visible at the top of the function page) before proceeding.
Before you test this function, what do you think will happen when it tries to call the S3 API now that it is inside a VPC with private subnets?
- Click the Test button to invoke your function.
The function times out. You will see a "Task timed out after 30.00 seconds" error. This is the designed obstacle. Your function is now trapped inside the VPC with no route to the internet.
This timeout is expected.
By default, Lambda has internet access. When you attach it to a VPC, it can only reach resources within that VPC. Connecting to a public subnet does NOT give Lambda internet access or a public IP. The S3 API endpoint lives on the public internet, so your function can no longer reach it.
Understand the networking fix (exam concept)
You just hit the most common VPC networking trap on the SAA-C03 exam. Here is the architecture that fixes it:
- Private subnet (where your Lambda ENIs live) routes traffic to a NAT Gateway in a public subnet.
- The NAT Gateway routes traffic to the Internet Gateway, which connects to the public internet.
- The full path: Lambda ENI (private subnet) -> NAT Gateway (public subnet) -> Internet Gateway -> Internet -> S3 API.
Why not just use a public subnet?
Connecting a function to a public subnet does not give it internet access or a public IP address. This is a key exam fact. The only way to give a VPC-attached Lambda internet access is through a NAT Gateway (or NAT instance) in a public subnet, with a route from the private subnets to that NAT Gateway.
For reaching AWS services specifically, VPC Endpoints are an alternative. The S3 Gateway Endpoint is free and lets your Lambda reach S3 without going over the public internet. On the exam, look for answers mentioning VPC Endpoints when the question is about reaching AWS services specifically.
Why not create a NAT Gateway right now?
NAT Gateways cost approximately $0.045 per hour plus data processing charges. For this learning exercise, understanding the concept is what matters for the exam. You have experienced the failure and now know the fix. In production, you would create a NAT Gateway or use a VPC Endpoint depending on your use case.
Configure IAM with least privilege
Regardless of network access, your function's execution role needs the correct IAM permissions to interact with S3. Right now it only has basic CloudWatch Logs permissions. You will add an inline policy scoped to your specific bucket.
- Navigate to the IAM console and click Roles in the left sidebar.
- Search for and click your Lambda execution role (starts with image-processor-role).
- Click Add permissions, then Create inline policy.
- Click the JSON tab and replace the contents with the following policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::lambda-trigger-yourname-2026",
"arn:aws:s3:::lambda-trigger-yourname-2026/*"
]
}
]
}
What does this policy do?
- s3:GetObject allows reading individual objects from the bucket.
- s3:ListBucket allows listing the contents of the bucket (which your code uses).
- The Resource ARNs scope these permissions to your specific bucket only. The first ARN targets the bucket itself (for ListBucket), and the second with /* targets objects inside it (for GetObject).
Replace lambda-trigger-yourname-2026 with the actual name of the S3 bucket you created in Step 2.
- Click Next.
- Enter a policy name such as S3BucketAccessPolicy.
- Click Create policy.
You should now see three policies attached to your role: AWSLambdaBasicExecutionRole (CloudWatch Logs), AWSLambdaVPCAccessExecutionRole (ENI management), and your new inline policy (S3 access).
What are the two sides of Lambda security?
Lambda security has two distinct sides that the exam tests:
- Execution role controls what Lambda can DO. It grants permissions to call other AWS services (S3, DynamoDB, SQS, etc.).
- Resource policy controls WHO can invoke Lambda. It grants other services (S3, API Gateway, CloudWatch) permission to trigger the function.
Your function already has a resource policy (from Steps 2 and 3) allowing S3 and API Gateway to invoke it. Now you have also configured the execution role to allow the function to access your S3 bucket.
Not seeing three policies on the role?
Make sure you are looking at the correct role. The role name starts with image-processor-role. Check that you attached AWSLambdaVPCAccessExecutionRole as a managed policy (not inline) earlier in this step. Help me check my Lambda execution role has the correct policies.
Your Lambda function is now attached to a VPC with properly configured IAM permissions. You experienced the classic VPC networking trap: a function that could reach S3 before now times out because private subnets have no internet route. On the exam, remember that the fix is either a NAT Gateway (for general internet access) or a VPC Endpoint (for AWS service access specifically).
Next up, you will configure concurrency controls and failure handling to make your function production-ready.
Configure Concurrency and Failure Handling
In the previous step you locked down your function's permissions and experienced how VPC networking affects Lambda. Now you need to protect your function from being overwhelmed and ensure that failed events are never silently lost.
Without concurrency controls, a sudden spike in S3 uploads could consume all 1,000 concurrent executions in your region, starving other functions. Without a Dead Letter Queue, any event that fails all retries simply disappears with no trace. Both scenarios are heavily tested on the SAA-C03 exam.
In this step, get ready to:
- Configure Reserved Concurrency to cap and guarantee execution capacity.
- Create an SQS Dead Letter Queue to catch failed async invocations.
- Test failure handling by forcing an exception and confirming the event lands in the DLQ.
Configure Reserved Concurrency
Reserved Concurrency is a free setting that does two things at once: it guarantees a number of concurrent executions for your function, and it caps the function at that same number. This protects downstream resources from being overwhelmed by a traffic spike.
- Return to the Lambda console and select your image-processor function.
- Choose the Configuration tab.
- In the left sidebar, choose Concurrency.
- Choose Edit.
- Under Reserved concurrency, enter 5.
- Choose Save.
The Concurrency section now shows Reserved concurrency set to 5. This confirms the function is both guaranteed and capped at 5 simultaneous executions.
Reserved vs Provisioned Concurrency
Reserved Concurrency is free. It caps your function's maximum concurrent executions AND guarantees that capacity from the region's pool. Setting it to 0 throttles all invocations, which is useful as an emergency kill switch.
Provisioned Concurrency is paid. It pre-warms execution environments so your function never experiences a cold start. SnapStart is an alternative for Java functions that achieves similar results.
On the exam, remember: Reserved = free cap + guarantee. Provisioned = paid warmth + no cold starts.
What about new account quotas?
New AWS accounts have reduced concurrency and memory quotas. AWS raises these automatically based on your usage. If you cannot set Reserved Concurrency to 5, your account quota may still be limited.
Create an SQS Dead Letter Queue
When Lambda invokes your function asynchronously (like from an S3 trigger) and the function fails all retry attempts, Lambda discards the event by default. A Dead Letter Queue catches those discarded events so you can inspect and reprocess them.
- Navigate to the Amazon SQS console.
- Choose Create queue.
- For Type, select Standard.
- For Name, enter image-processor-dlq.
- Leave all other settings at their defaults.
- Choose Create queue.
The SQS console shows your new image-processor-dlq queue with 0 messages available. Copy the ARN from the queue details. You will need it in the next substep.
Don't see the Create queue button?
Make sure you are in the SQS service console, not SNS. You can search for SQS in the top search bar to navigate directly.
help me find the SQS console in AWS
Configure the DLQ and test failure handling
Now you will wire the DLQ to your Lambda function's asynchronous invocation settings, grant the function permission to send messages to the queue, and then deliberately break the function to prove the DLQ catches the failure.
- Return to the Lambda console and select your image-processor function.
- Choose Configuration, then Asynchronous invocation in the left sidebar.
- Choose Edit.
- Set Maximum age of event to 1 hour.
- Set Retry attempts to 2.
- For Dead-letter queue service, choose Amazon SQS.
- Select your image-processor-dlq queue from the dropdown.
- Choose Save.
The Asynchronous invocation settings now show your DLQ ARN, 2 retry attempts, and a 1-hour maximum event age.
What about Destinations?
Destinations are a newer alternative to DLQs. They can route invocation records on success or failure to SQS, SNS, Lambda, or EventBridge. Destinations include more detail in the invocation record (the function's response, not just the original event).
DLQs are still widely used and appear frequently on the exam. Both are valid patterns for capturing failed events.
Next, grant the function's execution role permission to send messages to the DLQ.
- Navigate to the IAM console and choose Roles in the left sidebar.
- Search for and select your Lambda execution role (its name starts with image-processor-role).
- Choose Add permissions, then Create inline policy.
- Switch to the JSON editor and paste the following policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:REGION:ACCOUNT_ID:image-processor-dlq"
}
]
}
What does this policy do?
This grants your Lambda function the single permission it needs to write failed events to the DLQ. Replace the region and account ID in the Resource ARN with the ARN you copied from the SQS console earlier. The principle of least privilege means granting only sqs:SendMessage on only this specific queue.
- Replace REGION and ACCOUNT_ID with your actual values from the DLQ ARN you copied.
- Choose Next.
- Enter a policy name such as sqs-dlq-send.
- Choose Create policy.
The inline policy now appears in the role's Permissions policies list. Your function can send failed events to the DLQ.
Now you will deliberately break the function to trigger the DLQ path.
- Return to the Lambda console and open your image-processor function's Code tab.
- Add this line at the very top of your handler function (before any other logic):
# Temporarily force a failure to test DLQ
raise Exception("Intentional failure for DLQ testing")
Why raise an exception?
When Lambda invokes your function asynchronously and the function raises an unhandled exception, Lambda retries the invocation (up to your configured retry count of 2). After all retries are exhausted, Lambda sends the original event payload to the DLQ instead of discarding it.
- Choose Deploy to save the updated code.
The S3 trigger invokes your function asynchronously, which is exactly what you need to test the DLQ. Uploading a file to S3 will trigger an async invocation that hits the exception.
- Navigate to the S3 console and open your lambda-trigger-yourname-2026 bucket.
- Upload any small text file (for example, a file named dlq-test.txt).
Before you check the DLQ, think about what you expect to happen. Lambda will invoke your function, hit the exception, retry twice (with a wait of about 1 minute between the first two attempts and 2 minutes before the third), then route the event to the DLQ.
Wait 4-5 minutes before checking
Lambda retries with increasing delays: approximately 1 minute between the first two attempts, then 2 minutes before the final attempt. After all 3 attempts fail (the original plus 2 retries), the event is sent to the DLQ. Give it about 4-5 minutes total before polling.
Do you think the DLQ will contain the original S3 event payload, or something else entirely?
- Navigate to the SQS console.
- Select your image-processor-dlq queue.
- Choose Send and receive messages.
- Choose Poll for messages.
You should see 1 message available in the Messages section. Select the message to view its body. The message body contains the original S3 event payload (the bucket name and object key of the file you uploaded).
What is Lambda sending to the DLQ?
Lambda sends the original event to the Dead Letter Queue as-is, with additional information in message attributes. You can use those attributes to identify the error or correlate the event with CloudWatch logs.
This is different from Destinations, which send a richer invocation record that includes the function's error response.
No messages appearing in the DLQ?
Confirm you waited at least 4-5 minutes after uploading the file. Lambda needs time to exhaust all retries before routing to the DLQ.
Check that the inline policy on your execution role uses the correct DLQ ARN. If Lambda cannot send to the DLQ due to missing permissions, it silently discards the event and emits a DeadLetterErrors metric in CloudWatch.
help me debug why my Lambda DLQ is not receiving messages
Remember to revert your function code
Remove the raise Exception line from your handler and choose Deploy again. You do not want future S3 uploads to keep failing and filling the DLQ.
You have now configured production-grade resilience for your Lambda function. Reserved Concurrency protects against runaway scaling, and the Dead Letter Queue ensures no failed event vanishes without a trace. These are two of the most frequently tested serverless patterns on the SAA-C03 exam.
Ready for a challenge? In the Secret Mission, you will orchestrate multiple Lambda functions into a workflow using AWS Step Functions.
Secret mission
Build a Step Functions Workflow
Step Functions lets you orchestrate multiple Lambda functions into a visual workflow with conditional logic and error handling. In this mission, you will build a state machine that chains three Lambda functions together, adding a Choice state that routes execution based on success or failure.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Most services used in this project are pay-per-use and cost virtually nothing when idle, but the NAT Gateway charges hourly whether used or not.
Cost warning
The NAT Gateway costs approximately $0.045/hr plus data processing charges. If you completed Step 4 and left the NAT Gateway running, it has been charging you since creation. Delete it immediately if you are not continuing to experiment.
Lambda, API Gateway, S3, SQS, and Step Functions are all within free-tier limits for this project and cost nothing when idle.
Resources you used:
- NAT Gateway (hourly charges while it exists).
- Elastic IP address associated with the NAT Gateway.
- Lambda functions: image-processor, validate-input, process-data, notify-complete.
- API Gateway REST API: image-processor-api.
- S3 bucket: lambda-trigger-yourname-2026.
- SQS queue: image-processor-dlq.
- Step Functions state machine.
- IAM execution roles for Lambda and Step Functions.
Keep everything running
No action needed. Choose this if you are still actively experimenting with your serverless pipeline.
- Lambda, API Gateway, SQS, S3, and Step Functions are pay-per-use. They cost virtually nothing when idle.
- The NAT Gateway continues to charge ~$0.045/hr. If you are done with Step 4's VPC experiment, delete just the NAT Gateway and Elastic IP now (see the Delete tab for those specific steps) even if you keep everything else.
Pause - I'll come back to this later
Shut down the expensive resource while keeping your functions and configurations intact for later use.
- Delete the NAT Gateway immediately to stop hourly charges (see the Delete tab for steps).
- Release the associated Elastic IP address to avoid idle-IP charges.
- Remove the VPC configuration from the image-processor Lambda function (Configuration tab, VPC, Edit, remove subnets and security group, then Save). This avoids the function timing out when you test it later without the NAT Gateway.
- Keep all other resources. They cost nothing when idle.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps in order.
Delete the Step Functions state machine
- Open the Step Functions console and select **State machines** in the left navigation.
- Select your state machine, choose **Actions**, then **Delete**.
- Confirm the deletion.
Delete the SQS queue
- Open the SQS console, select image-processor-dlq.
- Choose **Delete**, type the queue name to confirm, then choose **Delete**.
Remove VPC configuration and delete the NAT Gateway
- In the Lambda console, open the image-processor function. Go to **Configuration**, then **VPC**, then **Edit**. Remove all subnets and the security group, then **Save**.
- Open the VPC console. In the navigation pane, choose **NAT gateways**.
- Select your NAT gateway, choose **Actions**, then **Delete NAT gateway**.
- Enter delete to confirm, then choose **Delete**.
- Open the EC2 console. In the navigation pane, choose **Elastic IPs**.
- Select the Elastic IP that was associated with your NAT gateway, choose **Actions**, then **Release Elastic IP address**. Confirm the release.
Delete the API Gateway REST API
- Open the API Gateway console. On the **APIs** page, select image-processor-api.
- Choose **API actions**, then **Delete API**.
- Enter confirm in the dialog box, then choose **Delete**.
Empty and delete the S3 bucket
- Open the S3 console, select your lambda-trigger-yourname-2026 bucket.
- Choose **Empty**, type permanently delete to confirm, then choose **Empty**.
- Return to the bucket list, select the bucket again, and choose **Delete**. Enter the bucket name to confirm.
Delete the Lambda functions
- Open the Lambda console Functions page.
- Select image-processor, choose **Actions**, then **Delete**. Type confirm and choose **Delete**.
- Repeat for validate-input, process-data, and notify-complete.
Delete the IAM execution roles
- Open the IAM console and choose **Roles**.
- Search for roles containing image-processor. Select each role, choose **Delete**, enter the role name to confirm, then choose **Delete**.
- Repeat for any Step Functions execution role (search for the state machine name).
- Delete any roles associated with validate-input, process-data, and notify-complete.
Delete CloudWatch log groups
- Open the CloudWatch console and choose **Log groups**.
- Select the log groups starting with /aws/lambda/image-processor, /aws/lambda/validate-input, /aws/lambda/process-data, and /aws/lambda/notify-complete.
- Choose **Actions**, then **Delete log group(s)**. Confirm with **Delete**.
Nice Work!
Nice Work!
Nice work! You just built a fully wired event-driven serverless pipeline on AWS, covering the most heavily tested Lambda topics on the SAA-C03 exam.
You've learned how to:
- Wire S3 event triggers and a REST API in API Gateway to invoke a Lambda function, demonstrating the two most common serverless integration patterns.
- Experience the VPC networking trap firsthand, where Lambda loses internet access in a VPC without a NAT Gateway or VPC Endpoint.
- Configure production resilience with Reserved Concurrency and a Dead Letter Queue so failed async invocations are captured instead of silently lost.
- Build a Step Functions state machine that orchestrates multiple Lambda functions with conditional branching using a Choice state (Secret Mission).
Ready to quiz yourself?