Build a Simple Serverless Upload Flow with S3 + Lambda
Hands-on lab: upload a file to S3, trigger a Lambda function, read the event, and verify the result in CloudWatch Logs.
All services used in this lesson are covered by the AWS Free Tier.
AWS Services Used
What you are building
Amazon S3 can publish event notifications when things happen in a bucket, and one supported destination is AWS Lambda. A common example is an ObjectCreated event that invokes a Lambda function when a file is uploaded. The Lambda function must be in the same Region as the S3 bucket.
S3 invokes the function asynchronously and passes a JSON event that includes details such as the bucket name and object key.
What you will build
- One S3 bucket for uploads
- One Lambda function
- One S3 trigger for
ObjectCreatedevents - One safe prefix like
incoming/ - One CloudWatch Logs view to confirm the function ran
Safety rule before you start
Warning
If your Lambda function writes back to the same bucket that triggers it, you can accidentally create a recursive loop. Use two buckets or restrict the trigger to a specific incoming prefix and never write back into that prefix.
For this lab, use a trigger prefix of incoming/ and only upload files into that prefix. Do not have the function write back into incoming/.
Part 1: Create the S3 bucket
Create a general purpose bucket in the Region you want to use. Make sure you remember the Region, because the Lambda function must be created in that same Region.
A good name pattern:
yourname-serverless-upload-12345
Leave the bucket private. This lab does not need public website access.
Part 2: Create the Lambda function
Create a Lambda function in the same Region as the bucket.
For this first version, use simple Python code like this:
import json
import urllib.parse
def lambda_handler(event, context):
records = event.get("Records", [])
for record in records:
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(
record["s3"]["object"]["key"], encoding="utf-8"
)
event_name = record["eventName"]
print(f"Event: {event_name}")
print(f"Bucket: {bucket}")
print(f"Key: {key}")
return {
"statusCode": 200,
"body": json.dumps({"processed_records": len(records)})
}
This code reads the S3 event data and logs it so you can verify the end-to-end flow.
Part 3: Use the right execution role
If your function only logs the event, the basic Lambda logging permissions are enough. If your function later needs to read the uploaded object from S3, then it also needs s3:GetObject permission in its execution role.
Tip
For a later upgrade, move bucket names or operational settings into environment variables instead of hard-coding them.
Part 4: Add the S3 trigger
Open the Lambda function and add an S3 trigger. Choose:
- Your bucket
- Event type:
ObjectCreated - Prefix:
incoming/
This prefix matters. It means only uploads whose keys begin with incoming/ will trigger the function, which is exactly the loop-avoidance pattern.
Part 5: Upload a test file
Upload a file into the bucket using a key like:
incoming/hello.txt
You can put a small line inside the file:
Hello from my first serverless upload flow.
Part 6: Verify the result in CloudWatch Logs
After the upload, open the Lambda function's monitoring area or CloudWatch Logs and inspect the latest log stream.
You should see log lines similar to:
- The event type
- The bucket name
- The object key
That confirms:
- The S3 event notification fired
- Lambda received the event
- Your function parsed the event correctly
Verify your work
| Step | Success condition |
|---|---|
| Create bucket | Bucket exists in the same Region you will use for Lambda |
| Create Lambda function | Function exists and can write logs |
| Add S3 trigger | Trigger listens for ObjectCreated on incoming/ |
| Upload test file | incoming/hello.txt is uploaded |
| Check logs | You see the bucket name and key in CloudWatch Logs |
| Avoid recursion | Function does not write back into the triggering prefix |
Micro-activity 1: Event reading
Think about it
After your upload, reflect: What bucket name did Lambda log? What object key did Lambda log? What event name did Lambda log? Did the upload happen inside the trigger prefix?
Micro-activity 2: Prevent the loop
Think about it
Why is writing back to the same triggering bucket risky? What is one safe way to avoid that problem? Think about what happens when a Lambda function writes an object that re-triggers itself.
Summary
In this lesson, you built a basic event-driven AWS workflow: an S3 upload created an event notification, Lambda received the event asynchronously, and the result was visible in CloudWatch Logs. This is one of the cleanest examples of serverless architecture because it shows storage, events, compute, and observability working together.
You also learned the most important safety rule for S3-triggered Lambda functions: avoid recursion by using two buckets or by limiting the trigger to an incoming prefix.