Build a Serverless Visitor Counter
Build a public Lambda visitor counter with DynamoDB and CloudWatch logs.
Introduction
30 Second Summary
A resume can describe your technical skills. A working link gives someone proof they can test in seconds.
In this project, you will build a browser-accessible visitor counter with AWS Lambda and Amazon DynamoDB. You will turn a fixed display into a persistent count that increases with each refresh.
What You'll Build
You will open a public HTTPS address and see a styled resume card whose visitor count climbs every time you refresh the page.
By the end of this project, you'll have:
- A live visitor counter that turns each browser refresh into a higher visible number.
- Clear proof of persistent state through a stored count that survives separate function requests.
- A portfolio evidence package with the live page, stored data, scoped permissions, invocation logs, and a concise README.md.
- Secret Mission: Add a /health route that reports service status without increasing the visitor count.
Are there any prerequisites?
You need an AWS account with permission to create Lambda functions, DynamoDB tables, IAM inline policies, and CloudWatch log groups.
AWS CloudShell handles the terminal work in your browser, so you do not need local development tools on your Mac.
Before We Start
This is your moment to commit to the serverless resume visitor counter you are about to build. Naming its audience and purpose gives the technical work ahead a clear goal.
Verify a Cost-Safe AWS Workspace
A public visitor counter needs account access you can trust. Choosing one AWS Region before any resources exist gives the build a consistent home.
The AWS Management Console gives you access to a browser-based terminal through AWS CloudShell. You can verify the required tools without installing software on your Mac.
In this step, get ready to:
- Confirm access to your AWS account in the project Region.
- Verify every required command in the CloudShell terminal.
- Prepare a CloudShell workspace with clear cost boundaries.
Select the project Region
Your credentials stay inside the AWS sign-in flow. This project never asks you to copy them into CloudShell.
- Sign in to the AWS Management Console in your browser.
- Complete any account verification requested during sign-in.
- Select US East (N. Virginia) as the Region.
- Confirm that the console session shows us-east-1.
Every project resource you create later stays in the same Region. This prevents a table or function from ending up somewhere unexpected.
Region unavailable?
- Confirm that your AWS account has permission to work in us-east-1.
- Ask your AWS account administrator to enable access if the Region remains unavailable.
Help me troubleshoot AWS Region access.
Verify the CloudShell toolchain
CloudShell forwards your signed-in session to its managed terminal. The AWS CLI can use that session without local access keys.
Your first CloudShell session can take a short moment to prepare. That pause is AWS creating the managed shell environment.
- Launch AWS CloudShell from the console toolbar.
- Wait for the terminal prompt to become available in the CloudShell panel.
- Keep local access keys out of this project.
Before you run the checks, predict whether every command can return version information.
- Verify the four installed tools by running these commands:
python3 --version
pip3 --version
zip --version
aws --version
What Do These Checks Prove?
- The python3 check confirms that the Python runtime is available.
- The pip3 check confirms that the Python package installer is available.
- The zip check confirms that CloudShell can build a deployment archive.
- The AWS CLI check confirms that the browser terminal can run AWS commands.
Each command prints installed version information instead of an error. Your browser terminal now has the complete toolchain needed for the project.
One of the checks failed?
- Confirm that you ran the checks in the CloudShell browser terminal instead of the Terminal app on your Mac.
- Wait for the CloudShell prompt to finish loading before trying the checks again.
Help me fix a CloudShell command check.
Set the project boundary
A dedicated directory keeps the deployment files for this counter together. Storing it in the CloudShell home directory also gives the project a consistent working location.
- Create the resume-visitor-counter directory in your CloudShell home directory and enter it by running these commands:
mkdir ~/resume-visitor-counter
cd ~/resume-visitor-counter
What Do These Commands Do?
- The mkdir command creates the project directory under your CloudShell home directory.
- The cd command makes that directory the active location for later project files.
The workspace also needs a clear boundary around services that add cost or complexity. Recording that boundary now guides every resource choice later.
What Keeps This Workspace Cost-Safe?
AWS CloudShell has no additional charge. This step creates no project resources.
The project excludes EC2, NAT gateways, load balancers, provisioned concurrency, and optional Amazon DynamoDB features. This boundary keeps the build focused on the counter's core flow.
- Record the project's cost boundary in the checkpoint below.
Before the final check, predict which directory name the terminal should report.
- Confirm the active CloudShell directory by running this command:
pwd
What Does This Check Show?
The pwd command prints the directory where new files and commands are placed. Its output confirms that CloudShell is using your project workspace.
The output ends with resume-visitor-counter. That confirms the directory exists and is ready for the project files.
Seeing a different directory?
- Run the cd command from the earlier block again if the path ends somewhere else.
- Confirm that the directory name uses lowercase letters with hyphens between the words.
Help me enter the correct CloudShell directory.
Your browser-based AWS workspace is ready without a local setup on your Mac. Next, you will launch the deliberately forgetful visitor counter.
Launch a Stateless Counter
Your browser-based AWS workspace is ready. You can now turn that setup into an AWS Lambda function that serves a resume visitor page.
This first counter uses a hard-coded value. Refreshing its public Lambda Function URL exposes the missing persistent state that the next step must solve.
In this step, get ready to:
- Create ResumeVisitorCounter with Python 3.14.
- Deploy a styled page with a fixed visitor count.
- Publish the page through a public Function URL.
Create the Lambda function
A Lambda function runs your Python handler when it receives an invocation. The function also needs an execution role so it can write basic runtime logs.
- Switch back to the AWS Management Console tab from earlier.
- Confirm the Region selector shows us-east-1.
- Enter AWS Lambda in the console search bar.
- Select AWS Lambda from the search results.
- Choose Create function.
- Select Author from scratch.
What is the default execution role?
An execution role is an AWS Identity and Access Management (IAM) role that the function assumes while it runs. The new default role includes basic permissions for CloudWatch Logs.
- Enter ResumeVisitorCounter in the Function name field.
- Choose Python 3.14 for Runtime.
- Select x86_64 under Architecture.
- Leave Change default execution role collapsed so Lambda creates a new default role.
- Choose Create function.
That is your first cloud resource in place. AWS Lambda now shows the ResumeVisitorCounter function with the Python 3.14 runtime.
Deploy and test the fixed counter
A Function URL expects the handler to return an HTTP response. This version returns a successful response containing a complete webpage with a fixed count.
- Choose the Code tab.
- Select lambda_function.py in the code editor.
- Replace the generated code by pasting the code below:
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "text/html; charset=utf-8"},
"body": """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Resume Visitor Counter</title>
<style>
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #0f172a; color: #e2e8f0; font-family: Arial, sans-serif; }
main { width: min(32rem, calc(100% - 2rem)); padding: 2.5rem; border: 1px solid #334155; border-radius: 1rem; background: #1e293b; box-sizing: border-box; text-align: center; }
.count { margin: 1rem 0; color: #38bdf8; font-size: 4rem; font-weight: 700; }
</style>
</head>
<body>
<main>
<p>AWS serverless portfolio project</p>
<h1>Resume Visitor Counter</h1>
<div class="count">Visitor count: 1</div>
<p>Recorded visits</p>
</main>
</body>
</html>""",
}
What does this code do?
- The lambda_handler() function receives each invocation from Lambda.
- The statusCode value tells the caller that the request succeeded.
- The Content-Type header tells the browser to render the response as HTML.
- The response body contains the page structure plus its CSS styling.
- The text Visitor count: 1 is embedded directly in the response.
- Choose Deploy to update the function code.
- Choose the Test tab.
- Choose Create new event under Test event.
Before you test, do you expect the handler to return a successful web response?
- Choose Test before saving to invoke the function.
- Expand Details under Execution result.
You should see a successful execution with statusCode set to 200. The response body contains the resume counter page.
Test not succeeding?
Confirm that lambda_function.py contains the complete handler above. Check that you chose Deploy after replacing the generated code.
If Lambda reports a Python syntax problem, compare the quotation marks around the multiline response body.
Help me debug my Lambda test.
✔️ Awesome, I've got everything!
Great. Double check that you deployed lambda_function.py successfully.
ⓧ I'd like to double check the full code
Your complete lambda_function.py file should match this version:
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "text/html; charset=utf-8"},
"body": """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Resume Visitor Counter</title>
<style>
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #0f172a; color: #e2e8f0; font-family: Arial, sans-serif; }
main { width: min(32rem, calc(100% - 2rem)); padding: 2.5rem; border: 1px solid #334155; border-radius: 1rem; background: #1e293b; box-sizing: border-box; text-align: center; }
.count { margin: 1rem 0; color: #38bdf8; font-size: 4rem; font-weight: 700; }
</style>
</head>
<body>
<main>
<p>AWS serverless portfolio project</p>
<h1>Resume Visitor Counter</h1>
<div class="count">Visitor count: 1</div>
<p>Recorded visits</p>
</main>
</body>
</html>""",
}
Publish and refresh the Function URL
A Function URL gives the $LATEST function version a direct HTTPS endpoint. Using NONE allows unsigned browser requests.
Publishing with NONE makes the page reachable without sign-in. This controlled demo has a pause and deletion path later in the project.
- Choose the Configuration tab.
- Choose Function URL.
- Choose Create function URL.
- Select NONE for Auth type.
- Choose Save.
You should see the new endpoint in the Function URL pane. This URL invokes ResumeVisitorCounter from any browser.
- Record the generated endpoint here: your-function-url-here.
- Open your-function-url-here in a new browser tab.
You should see a dark resume visitor card with Visitor count: 1 in blue.
Before you refresh, do you think the displayed number will behave like stored visit data?
- Refresh the visitor counter page three times.
You will see Visitor count: 1 after every refresh. That fixed result is the intended limitation of this version.
Each request generates a fresh response from the same hard-coded text. The function has no durable place to store the previous visit count.
Function URL not loading?
Confirm that the Function URL uses NONE for authentication. Check that the latest code was deployed before you created the URL.
Help me fix my Function URL.
Your public counter is live. Next, you will connect it to Amazon DynamoDB so each browser request updates durable data.
Persist the Count with DynamoDB
Your public AWS Lambda counter returns the same hard-coded value on every request. That fixed 1 proves the function needs durable state outside its execution environment.
Amazon DynamoDB provides that persistent state. Its atomic update adds one to the counter. The same operation returns the new value.
A least-privilege IAM policy limits the function to one table action. You will package the pinned dependency with the handler before deploying the persistent version.
In this step, get ready to:
- Create the provisioned visitor-count table.
- Grant the Lambda execution role one DynamoDB permission.
- Package the persistent handler before deploying it.
Create the persistent counter table
A DynamoDB table stores each item under a primary key. Your table uses counter_id to identify the resume counter.
Provisioned capacity gives this small project a fixed limit of 1 RCU plus 1 WCU. These values are the documented minimum for a provisioned table.
- Switch back to the AWS CloudShell session from earlier.
- Create the DynamoDB Standard table by running this command:
aws dynamodb create-table \
--table-name ResumeVisitorCount \
--attribute-definitions AttributeName=counter_id,AttributeType=S \
--key-schema AttributeName=counter_id,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
--table-class STANDARD
What does this command create?
- The ResumeVisitorCount name matches the table used by the handler.
- The counter_id string becomes the partition key for each item.
- The read capacity is set to 1 unit.
- The write capacity is set to 1 unit.
- The command creates no secondary indexes.
- Wait for the table to become usable by running this command:
aws dynamodb wait table-exists --table-name ResumeVisitorCount
What should the waiter show?
The waiter returns no output after the table becomes active. A fresh CloudShell prompt means you can continue.
- Return to the AWS Management Console in us-east-1.
- Open Amazon DynamoDB by using the console search bar.
- Select Tables from the navigation panel.
- Select ResumeVisitorCount from the table list.
- Confirm that the table status is ACTIVE.
- Copy the table ARN into your ResumeVisitorCount table ARN.
The table is ready to accept its first item. DynamoDB creates the resume item when the handler performs its first update.
Table creation blocked?
Confirm that CloudShell is operating in us-east-1. A table with the same name in another Region does not satisfy this project.
A permissions failure means your AWS account cannot create the table with its current permissions.
Help me troubleshoot the DynamoDB table creation.
Grant one DynamoDB permission
The function's execution role already permits logging. It now needs permission to update the new table.
Least privilege keeps that access narrow. The inline policy grants only dynamodb:UpdateItem against the table ARN you recorded.
- Switch back to the CloudShell session from earlier.
- Create iam-policy.json inside ~/resume-visitor-counter by running this command:
cat > iam-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UpdateResumeVisitorCounter",
"Effect": "Allow",
"Action": "dynamodb:UpdateItem",
"Resource": "your-table-arn-here"
}
]
}
EOF
What does this policy allow?
The statement permits the execution role to call dynamodb:UpdateItem. The resource placeholder marks where the table ARN belongs in the IAM copy.
The policy grants no table-reading permission. It also grants no table-deletion permission.
- Display the saved policy by running this command:
cat iam-policy.json
What should the file contain?
You should see one statement with the UpdateResumeVisitorCounter identifier. Its resource still contains your-table-arn-here.
- Return to ResumeVisitorCounter in the Lambda console.
- Select Configuration.
- Select Permissions.
- Select the role name under Execution role.
- Select Add permissions on the IAM role page.
- Select Create inline policy.
Where are you now?
You are editing permissions attached directly to the function's role. This inline policy applies when ResumeVisitorCounter uses that role.
- Select the editor that accepts JSON.
- Paste the contents of iam-policy.json into the editor.
- Replace your-table-arn-here with your ResumeVisitorCount table ARN.
- Continue to the policy review screen.
- Enter UpdateResumeVisitorCounter as the policy name.
- Create the inline policy.
- Confirm that the role permissions list includes the new inline policy.
Good work. The function can now update one table through one action.
Policy missing or rejected?
Confirm that you replaced the resource placeholder in the IAM editor. Keep the ARN on one line with its original punctuation.
Check that you added the policy to the execution role linked from ResumeVisitorCounter.
Help me fix the Lambda execution-role policy.
Package and deploy the persistent handler
The deployment ZIP must place the handler beside its dependencies at the archive root. Pinning Boto3 to 1.43.95 keeps the SDK version under your control.
The new handler uses one atomic update to increment the stored value. That operation returns the updated count for the HTML response.
- Switch back to the CloudShell session from earlier.
- Create requirements.txt inside ~/resume-visitor-counter by running this command:
cat > requirements.txt <<'EOF'
boto3==1.43.95
EOF
Why pin Boto3?
The dependency file requests one specific Boto3 release. Packaging that release avoids relying on the SDK copy supplied by the runtime.
- Confirm the dependency pin by running this command:
cat requirements.txt
What should the dependency file show?
The output should contain only boto3==1.43.95. This gives the deployment a repeatable SDK version.
You will assemble lambda_function.py in small sections. Each check shows the exact content CloudShell has saved so far.
- Write the imports plus DynamoDB configuration by running this command:
cat > lambda_function.py <<'PY'
import json
import logging
import boto3
from botocore.exceptions import ClientError
TABLE_NAME = "ResumeVisitorCount"
COUNTER_KEY = {"counter_id": "resume"}
COUNTER_ATTRIBUTE = "visits"
logger = logging.getLogger()
logger.setLevel("INFO")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)
PY
What does the foundation configure?
- The constants identify the table used by the handler.
- The resume value identifies the counter item.
- The visits value names the counter attribute.
- The logger records successful updates plus database failures.
- The DynamoDB resource creates the table object used by the handler.
- Append the opening HTML plus page layout by running this command:
cat >> lambda_function.py <<'PY'
HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Resume Visitor Counter</title>
<style>
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: #0f172a;
color: #e2e8f0;
font-family: Arial, sans-serif;
}
PY
What does this template section control?
The template starts the HTML document. Its grid layout centers the resume card in the browser.
- Inspect the first two sections by running this command:
cat lambda_function.py
What should the partial file show?
The output should begin with the imports plus table constants. It should end with the closing brace for the body style block.
- Append the card plus counter styling by running this command:
cat >> lambda_function.py <<'PY'
main {
width: min(32rem, calc(100% - 2rem));
padding: 2.5rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #1e293b;
box-sizing: border-box;
text-align: center;
}
.count {
margin: 1rem 0;
color: #38bdf8;
font-size: 4rem;
font-weight: 700;
}
.services {
color: #94a3b8;
line-height: 1.6;
}
PY
What does the card styling add?
The main styles shape the centered card. The .count styles make the stored value the strongest visual element.
- Append the page markup by running this command:
cat >> lambda_function.py <<'PY'
</style>
</head>
<body>
<main>
<p>AWS serverless portfolio project</p>
<h1>Resume Visitor Counter</h1>
<div class="count">__COUNT__</div>
<p>Recorded visits</p>
<p class="services">AWS Lambda + Amazon DynamoDB</p>
</main>
</body>
</html>
"""
PY
How does the page become dynamic?
The __COUNT__ placeholder marks where the current visit count appears. The handler replaces it before returning the page.
- Inspect the completed HTML template by running this command:
cat lambda_function.py
What should the template show?
The output should now contain the complete HTML document. Its final line should close the triple-quoted template.
- Append the response helper by running this command:
cat >> lambda_function.py <<'PY'
def build_response(status_code, content_type, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": content_type,
"Cache-Control": "no-store",
},
"body": body,
"isBase64Encoded": False,
}
PY
What does the response helper do?
The helper gives every response the required status plus content type. Its cache setting stops the browser from reusing an old counter page.
- Append the request-path checks by running this command:
cat >> lambda_function.py <<'PY'
def lambda_handler(event, context):
path = event.get("rawPath", "/")
if path == "/favicon.ico":
return build_response(204, "text/plain", "")
if path != "/":
return build_response(
404,
"application/json",
json.dumps({"message": "Not found"}),
)
PY
Why check the request path?
The handler reserves / for real visits. Browser favicon requests receive an empty response.
Unknown paths receive a JSON response. They never reach the counter update.
- Inspect the helper plus path checks by running this command:
cat lambda_function.py
What should the handler show so far?
The output should include build_response() followed by lambda_handler(). The handler should reject every path except the root path.
- Append the atomic update plus error handling by running this command:
cat >> lambda_function.py <<'PY'
try:
response = table.update_item(
Key=COUNTER_KEY,
UpdateExpression="ADD #counter :increment",
ExpressionAttributeNames={"#counter": COUNTER_ATTRIBUTE},
ExpressionAttributeValues={":increment": 1},
ReturnValues="UPDATED_NEW",
)
visits = int(response["Attributes"][COUNTER_ATTRIBUTE])
logger.info("Visitor count updated to %s", visits)
return build_response(
200,
"text/html; charset=utf-8",
HTML_TEMPLATE.replace("__COUNT__", str(visits)),
)
except ClientError:
logger.exception("DynamoDB update failed")
return build_response(
500,
"application/json",
json.dumps({"message": "Counter unavailable"}),
)
PY
How does the atomic counter work?
UpdateItem uses ADD to increase the visits attribute by one. DynamoDB creates the item when the resume key does not exist.
UPDATED_NEW returns the changed value from the same operation. The handler inserts that value into the HTML response.
ClientError turns a database failure into a structured response. The exception also reaches the function logs.
- Install the pinned dependency before building visitor-counter.zip by running these commands:
rm -rf package visitor-counter.zip
pip install --target ./package --requirement requirements.txt
cd package
zip -r ../visitor-counter.zip .
cd ..
zip visitor-counter.zip lambda_function.py
How is the ZIP assembled?
- The cleanup removes an older package directory plus deployment ZIP.
- The pip command installs the pinned dependency into package.
- The first zip command places the dependency files at the archive root.
- The second zip command adds lambda_function.py to that root.
- Confirm that the project files plus deployment archive exist by running this command:
ls
What should the directory contain?
You should see iam-policy.json, lambda_function.py, requirements.txt, plus visitor-counter.zip in the output.
- Deploy the ZIP to ResumeVisitorCounter by running these commands:
aws lambda update-function-code --function-name ResumeVisitorCounter --zip-file fileb://visitor-counter.zip
aws lambda wait function-updated --function-name ResumeVisitorCounter
What does the deployment command do?
The first command replaces the function code with your deployment archive. The waiter prevents the next request from reaching the previous version during the update.
A fresh CloudShell prompt confirms that Lambda has finished processing the deployment.
Before you refresh the page, what do you expect the displayed number to do now that root requests reach DynamoDB?
- Switch back to the public Function URL from earlier.
- Refresh the root page three times.
You should see the visitor count increase after every refresh. That is the persistence gap closed: each request now updates durable state.
Count missing or page failing?
Confirm that the inline policy uses the ARN for ResumeVisitorCount. A placeholder or different table ARN blocks the update.
Check that lambda_function.py sits at the root of visitor-counter.zip beside the dependency folders.
Help me debug the persistent Lambda counter.
✔️ Awesome, I've got everything!
Great work. Confirm that each root-page refresh increases the displayed count before moving on.
ⓧ I'd like to double check the full code
Compare your three source files with these complete versions. Keep the real table ARN in the IAM policy while leaving the reusable placeholder in iam-policy.json.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UpdateResumeVisitorCounter",
"Effect": "Allow",
"Action": "dynamodb:UpdateItem",
"Resource": "your-table-arn-here"
}
]
}
boto3==1.43.95
import json
import logging
import boto3
from botocore.exceptions import ClientError
TABLE_NAME = "ResumeVisitorCount"
COUNTER_KEY = {"counter_id": "resume"}
COUNTER_ATTRIBUTE = "visits"
logger = logging.getLogger()
logger.setLevel("INFO")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)
HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Resume Visitor Counter</title>
<style>
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: #0f172a;
color: #e2e8f0;
font-family: Arial, sans-serif;
}
main {
width: min(32rem, calc(100% - 2rem));
padding: 2.5rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #1e293b;
box-sizing: border-box;
text-align: center;
}
.count {
margin: 1rem 0;
color: #38bdf8;
font-size: 4rem;
font-weight: 700;
}
.services {
color: #94a3b8;
line-height: 1.6;
}
</style>
</head>
<body>
<main>
<p>AWS serverless portfolio project</p>
<h1>Resume Visitor Counter</h1>
<div class="count">__COUNT__</div>
<p>Recorded visits</p>
<p class="services">AWS Lambda + Amazon DynamoDB</p>
</main>
</body>
</html>
"""
def build_response(status_code, content_type, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": content_type,
"Cache-Control": "no-store",
},
"body": body,
"isBase64Encoded": False,
}
def lambda_handler(event, context):
path = event.get("rawPath", "/")
if path == "/favicon.ico":
return build_response(204, "text/plain", "")
if path != "/":
return build_response(
404,
"application/json",
json.dumps({"message": "Not found"}),
)
try:
response = table.update_item(
Key=COUNTER_KEY,
UpdateExpression="ADD #counter :increment",
ExpressionAttributeNames={"#counter": COUNTER_ATTRIBUTE},
ExpressionAttributeValues={":increment": 1},
ReturnValues="UPDATED_NEW",
)
visits = int(response["Attributes"][COUNTER_ATTRIBUTE])
logger.info("Visitor count updated to %s", visits)
return build_response(
200,
"text/html; charset=utf-8",
HTML_TEMPLATE.replace("__COUNT__", str(visits)),
)
except ClientError:
logger.exception("DynamoDB update failed")
return build_response(
500,
"application/json",
json.dumps({"message": "Counter unavailable"}),
)
Your public counter now remembers every root-page request. Next, you will prove the browser-to-database flow through the stored item plus function logs.
Verify the Data Flow
Your public counter now uses AWS Lambda to update persistent state in Amazon DynamoDB.
A browser result alone cannot prove that every layer worked.
You will match the browser count to the durable value. You will capture separate evidence from Amazon CloudWatch Logs and AWS Identity and Access Management (IAM).
In this step, get ready to:
- Prove that three browser requests increase the stored count by three.
- Capture browser, database, logging, and permission evidence.
- Create the project README and download your evidence package.
Prove the counter persists
An atomic counter changes the stored value during each root request. A before-and-after comparison connects your browser refreshes to that durable update.
- Return to Amazon DynamoDB in the AWS Management Console for us-east-1.
- Select Tables.
- Select ResumeVisitorCount.
- Open the table's item view.
- Find the item where counter_id is resume.
- Record the current visits value here: your current visits value.
Before you refresh, predict whether the stored count rises by exactly three.
- Switch back to the Function URL from earlier.
- Refresh the root page three times.
- Return to the ResumeVisitorCount item view.
- Reload the item view to fetch the current value.
You should see the visits value increase by three. That change proves that the browser requests reached durable storage.
Trace requests through logs and permissions
A reliable project story needs evidence from each layer. The stored item proves persistence while the invocation logs prove that your handler processed the requests.
- Return to ResumeVisitorCounter in AWS Lambda.
- Follow the function's CloudWatch logs link to /aws/lambda/ResumeVisitorCounter.
- Open the newest log stream containing your three requests.
- Find entries containing Visitor count updated to.
- Capture a screenshot of one successful invocation log.
- Switch back to the Function URL from earlier.
- Capture a screenshot of the styled visitor counter.
- Return to the resume item in ResumeVisitorCount.
Protect Account Details
Console screenshots can include account identifiers near the page header or resource details. Crop or redact those identifiers before keeping each image.
- Capture a screenshot showing the updated visits value.
- Return to ResumeVisitorCounter in AWS Lambda.
- Select Configuration.
- Select Permissions.
- Follow the Execution role link.
- Open the inline policy attached earlier.
- Capture a screenshot showing dynamodb:UpdateItem under Action.
- Confirm that the screenshot shows only the ResumeVisitorCount table under Resource.
You now have four views of the same system at work. The browser, table, log entry, and scoped permission each prove a different part of the request flow.
Package the portfolio evidence
A README.md turns your screenshots and source files into a clear technical story. It explains the architecture before a reviewer needs to inspect the implementation.
- In the AWS CloudShell editor from earlier, create README.md inside ~/resume-visitor-counter by pasting this content:
# Serverless Resume Visitor Counter
A browser-accessible visitor counter built with AWS Lambda, a Lambda Function URL, and Amazon DynamoDB.
## Architecture
Browser request -> Lambda Function URL -> Python Lambda handler -> DynamoDB atomic counter -> HTML response
## What it demonstrates
- Serverless HTTP delivery without a web server
- Persistent state outside a stateless compute environment
- DynamoDB atomic updates
- Least-privilege IAM permissions
- CloudWatch logging and cost-aware cleanup
## Demo
Function URL: your-function-url-here
The root path increments and displays the visitor count. The function ignores favicon requests and rejects unknown paths.
## Resume bullet
Built a serverless AWS visitor counter using Lambda Function URLs, Python, DynamoDB atomic updates, least-privilege IAM, and CloudWatch logging while keeping the workload within ongoing free allowances.
What Does This README Prove?
- The architecture section follows a request from the browser to the HTML response.
- The capability list connects the implementation to serverless development skills.
- The demo section explains how the public endpoint behaves.
- The resume bullet condenses the project into one outcome-focused statement.
- Save README.md.
- Check the CloudShell file list for README.md beside the existing project files.
- Keep your-function-url-here unchanged in this private evidence copy.
README Missing or Incomplete?
- Confirm that the editor saved README.md inside ~/resume-visitor-counter.
- Check that the file starts with # Serverless Resume Visitor Counter.
If the saved file still differs, help me compare my README with the expected project file.
✔️ Awesome, I've got everything!
Your README.md now explains the architecture, capabilities, demo behavior, and resume story.
ⓧ I'd like to double check the full code
- Compare your complete README.md with this reference:
# Serverless Resume Visitor Counter
A browser-accessible visitor counter built with AWS Lambda, a Lambda Function URL, and Amazon DynamoDB.
## Architecture
Browser request -> Lambda Function URL -> Python Lambda handler -> DynamoDB atomic counter -> HTML response
## What it demonstrates
- Serverless HTTP delivery without a web server
- Persistent state outside a stateless compute environment
- DynamoDB atomic updates
- Least-privilege IAM permissions
- CloudWatch logging and cost-aware cleanup
## Demo
Function URL: your-function-url-here
The root path increments and displays the visitor count. The function ignores favicon requests and rejects unknown paths.
## Resume bullet
Built a serverless AWS visitor counter using Lambda Function URLs, Python, DynamoDB atomic updates, least-privilege IAM, and CloudWatch logging while keeping the workload within ongoing free allowances.
What Should Match?
Check every heading, list item, placeholder, and sentence. Your saved file should match this reference exactly.
- Replace your-function-url-here only in a separate copy that you publish while the endpoint remains active.
Your written project story is ready. The final package brings it together with the code and visual proof.
- Use the CloudShell file download control to download lambda_function.py.
- Use the CloudShell file download control to download requirements.txt.
- Use the CloudShell file download control to download iam-policy.json.
- Use the CloudShell file download control to download visitor-counter.zip.
- Use the CloudShell file download control to download README.md.
- Move the five downloaded files into one Finder folder with your four screenshots.
Before you check the folder, predict which artifact proves each layer of the system.
- Open the evidence folder in Finder.
You should see five project files. You should also see four screenshots covering the browser, table, logs, and scoped IAM permission.
Strong finish. You now have durable proof of the complete request, compute, storage, response, permission, and logging flow.
Secret mission
Add a Non-Counting Health Check
Monitoring should confirm that your service is available without changing visitor data. Add a dedicated `/health` route that returns a status response while leaving the stored count unchanged.
Clean Up Your Resources
Clean Up Your Resources
Your AWS project is expected to stay within its documented monthly free allowances at this size. Decide whether to keep your resources available, pause the public endpoint, or delete everything.
Cost warning
Your public AWS Lambda Function URL accepts unsigned traffic because its auth type is NONE. Unexpected requests can increase Lambda requests beyond the monthly allowance.
Your Amazon DynamoDB table also uses an ongoing allowance. Your Amazon CloudWatch Logs usage can grow as requests produce more logs.
Choose Pause or Delete if you have finished demonstrating the project.
Resources you used:
- Public ResumeVisitorCounter Lambda function with a $LATEST Function URL using auth type NONE.
- Provisioned-capacity ResumeVisitorCount DynamoDB Standard table using 1 RCU and 1 WCU.
- Lambda execution role in AWS Identity and Access Management (IAM) with the default logging policy and the inline dynamodb:UpdateItem permission.
- CloudWatch log group named /aws/lambda/ResumeVisitorCounter.
- The ~/resume-visitor-counter project directory in AWS CloudShell containing your source files, deployment package, README, and portfolio screenshots.
Keep everything running
No action is required. Choose this option while you are actively demonstrating the project or refining its portfolio story.
- Keep traffic to the public Function URL very low.
- Monitor your AWS usage for unexpected requests.
- Keep the ResumeVisitorCount table available so the visitor total remains persistent.
- Keep ~/resume-visitor-counter in CloudShell so you can update the source package later.
- Keep your downloaded screenshots and source files as evidence of the completed data flow.
Pause - I'll come back to this later
Pausing sets the function's reserved concurrency to zero while preserving its code and related resources. The public URL remains configured for your return.
- Switch back to AWS CloudShell from earlier.
- Pause ResumeVisitorCounter by running this command:
aws lambda put-function-concurrency --function-name ResumeVisitorCounter --reserved-concurrent-executions 0
What does this command do?
Reserved concurrency controls how many Lambda executions can run at once. Setting it to 0 intentionally throttles the function without deleting its code.
- Refresh the root Function URL in your browser.
You should receive HTTP 429. This confirms that Lambda has stopped processing requests.
- Keep the ResumeVisitorCount table and ~/resume-visitor-counter directory for your return.
- Remove the reserved concurrency limit when you are ready to demonstrate the endpoint again.
Delete - I don't want to use this again
Deletion permanently removes the public function, stored visitor count, logs, execution role, and CloudShell project directory. Choose this option when you no longer need the live demo.
Keep your evidence first
The CloudShell directory contains the source files and screenshots that support your portfolio story. Files removed from this directory are not part of the downloaded evidence on your Mac.
- Download any remaining source files or screenshots you want to keep.
- Switch back to AWS CloudShell from earlier.
- Delete the Lambda function, DynamoDB table, and CloudWatch log group by running these commands:
aws lambda delete-function --function-name ResumeVisitorCounter
aws dynamodb delete-table --table-name ResumeVisitorCount
aws logs delete-log-group --log-group-name /aws/lambda/ResumeVisitorCounter
What do these commands remove?
- The first command deletes ResumeVisitorCounter and its public Function URL.
- The second command deletes the ResumeVisitorCount table and its stored resume item.
- The third command deletes /aws/lambda/ResumeVisitorCounter and its invocation logs.
- Return to the Lambda function list from earlier.
- Confirm that ResumeVisitorCounter is absent.
- Return to the DynamoDB table list from earlier.
- Confirm that ResumeVisitorCount is absent.
- Return to the CloudWatch log group list from earlier.
- Confirm that /aws/lambda/ResumeVisitorCounter is absent.
That clears the public compute, persistent counter, and project logs from AWS.
- Return to IAM in the AWS Management Console.
- Select Roles.
- Choose the execution role used by ResumeVisitorCounter.
- Use the role deletion control to remove the unused role.
- Confirm that the execution role is absent from the roles list.
- Delete the CloudShell project directory by running this command:
rm -rf ~/resume-visitor-counter
What does this command remove?
This command permanently removes ~/resume-visitor-counter from CloudShell. It includes lambda_function.py, requirements.txt, iam-policy.json, README.md, visitor-counter.zip, and any screenshots stored there.
Nice Work!
Nice Work!
You did it! Your public resume visitor counter now proves a complete browser-to-database flow.
You've learned how to:
- Delivered a public visitor-counter webpage through an AWS Lambda Function URL. Visitors can refresh the root page to see the displayed count rise.
- Used an Amazon DynamoDB atomic counter to preserve visits across Lambda executions. The update increments the stored value. It returns that value in the same operation.
- Packaged portfolio evidence in README.md. It shows the browser result. It also shows scoped AWS Identity and Access Management (IAM) access. It includes Amazon CloudWatch Logs evidence.
- Secret Mission: Added a /health route that reports service status. Repeated health checks leave visits unchanged.
Ready to quiz yourself?