AWS Builder Center

AWS: Zero to Hero — Week 7: Serverless & Event-Driven AWS — Lambda, API Gateway, SQS, SNS, EventBridge

Week 7 of learning AWS from zero, with Claude as my research partner. Lambda, API Gateway (HTTP vs REST), SQS, SNS fan-out, and EventBridge explained. Then a real event-driven lab: one HTTP request triggers an SNS publish that fans out to two SQS queues, each processed by its own Lambda, plus a fourth Lambda on an EventBridge schedule — all five services, actually wired together.

Series: AWS: Zero to Hero (9 articles)

  1. …
  2. 7
    AWS: Zero to Hero — Week 7: Serverless & Event-Driven AWS — Lambda, API Gateway, SQS, SNS, EventBridge This article
Six weeks of this series have been about things that sit somewhere and wait to be used — servers, disks, databases, networks. This week is a genuine shift in mindset: what does it actually mean to build something where you never provision a server at all, and where different parts of your system don't even talk to each other directly?

The Deep Dive

What "serverless" actually means

"Serverless" doesn't mean no servers exist — it means you never provision, patch, or scale them yourself. You write a function, AWS runs it only when something triggers it, and you pay per invocation and per millisecond of execution — nothing while it's idle. This is Week 1's elasticity taken to its logical extreme: not "scales automatically," but "scales to literally zero when nobody's using it."

Lambda: the unit of serverless compute

Lambda runs a function in response to an event — an HTTP request, a file landing in S3, a message arriving in a queue, a schedule firing. A few mechanics worth actually understanding:
  • Cold starts vs. warm starts — the first invocation after a period of inactivity has to initialize your function's environment first, adding real latency; subsequent invocations reuse that warm environment and skip the init cost. This is the main practical tradeoff serverless asks you to accept.
  • Memory and CPU are linked — you configure memory (128 MB up to 10,240 MB), and CPU scales proportionally with it. A function that's slow because it's CPU-bound sometimes gets cheaper to run at higher memory, because it finishes faster than the extra memory costs.
  • 15-minute maximum timeout — Lambda is built for bounded, event-triggered work, not long-running processes. Anything that needs to run longer belongs on EC2, ECS, or (for multi-step workflows that wait on external events) Step Functions, which — worth remembering — doesn't bill you for the time it spends idly waiting the way a Lambda function stuck in a loop would.
  • Graviton (arm64) functions run at roughly a 20% lower duration rate than x86 — the same Nitro-team silicon from Week 2 and Week 4, showing up again here.
  • Function URLs — the simplest way to expose a Lambda over HTTPS directly, with no separate API Gateway resource at all, and no extra charge beyond Lambda's own request and duration pricing. For a single simple endpoint, this is often genuinely enough.
And a detail worth knowing precisely because it's easy to miss in the pricing page: Lambda's free tier — 1 million requests and 400,000 GB-seconds of compute every month — is Always Free, permanently, for every account regardless of when it was created. Not a 12-month clock, not a credit balance. The safest compute service in this entire series to experiment with.

API Gateway: REST vs. HTTP APIs

API Gateway puts a managed front door in front of Lambda (or any backend) — handling routing, throttling, authentication, and request validation so your function doesn't have to. AWS actually offers two meaningfully different API types now:
  • HTTP APIs — newer, simpler, and roughly a third of the price (~$1.00 per million requests vs. REST's ~$3.50 per million). This is the default recommendation for most new builds today.
  • REST APIs — the older, more feature-complete option: API keys, usage plans, request/response validation, and native AWS WAF integration. Worth the extra cost specifically when you need one of those features, not by default.
  • WebSocket APIs — a genuinely different use case: persistent, bidirectional connections, for things like live chat or real-time dashboards, priced per message and per connection-minute rather than per request.

SQS: decoupling by queueing

SQS (Simple Queue Service) sits between a producer and a consumer so they never have to be available at the same time. A producer drops a message in; a consumer picks it up whenever it's ready. Two mechanics worth knowing:
  • Visibility timeout — when a consumer picks up a message, it doesn't get deleted immediately — it becomes temporarily invisible to other consumers. If the consumer finishes and explicitly deletes it, it's gone for good. If it doesn't (a crash, a timeout), the message reappears after the visibility timeout expires and gets picked up again — this is what gives SQS its at-least-once delivery guarantee.
  • Dead-letter queues — a safety net for messages that keep failing processing after a set number of retries, so a single malformed message can't loop forever and quietly block an entire queue.
  • Standard vs. FIFO — Standard queues are near-unlimited throughput with at-least-once (occasionally duplicate) delivery; FIFO queues guarantee strict ordering and exactly-once processing, at a real throughput ceiling in exchange.

SNS: fan-out, the pattern SQS alone can't do

SNS (Simple Notification Service) is publish/subscribe: one message published to a topic gets pushed out immediately to every subscriber — which can be an SQS queue, a Lambda function, an email address, an SMS number, or an HTTP endpoint, all at once. This is the fan-out pattern, and it's the clean answer to "I need three completely different things to happen when one event occurs" — publish once, let SNS push it to everyone who's subscribed, instead of your producer needing to know or care who's listening.
The SNS-vs-SQS distinction that actually matters: SNS pushes immediately to every subscriber the moment a message is published; SQS holds a message until a consumer actively comes and pulls it. They're not competitors — SNS fanning out into multiple SQS queues, one per downstream system, is one of the single most common serverless architecture patterns that exists, and it's exactly what we're building in the lab below.

EventBridge: pattern-matching instead of topic names

EventBridge takes the same decoupling idea a step further. Instead of publishers and subscribers agreeing on a specific topic name, EventBridge routes events through rules that match on the actual content and structure of an event — an event bus receives events from over 200 AWS services (and SaaS partners), and a rule like "match any event where detail-type is OrderShipped" fires regardless of which service or account actually produced it.
Two extra pieces worth knowing exist:
  • EventBridge Scheduler — the modern, serverless answer to "run this on a schedule," replacing the old pattern of a cron job babysat on an EC2 instance just to fire something periodically. That's exactly what the lab below uses.
  • EventBridge Pipes — point-to-point integrations (SQS straight into a target, for example) without needing a Lambda function in the middle purely to move data from A to B, cutting out compute cost and latency for the simplest connections.

Putting the pattern together

Every one of these services is solving the same underlying problem from a different angle: let independent pieces of a system react to things happening, without any of them waiting on each other or needing to know who else is listening. That's genuinely what "integrating cloud into an app" looks like in practice once you're past a single server calling a single database — not one long chain of direct calls, but a web of producers and reactors, decoupled on purpose.

Cost Reality Check for This Week

This is one of the cheapest weeks in the whole series to actually run. Lambda, SQS, and SNS all have generous, genuinely Always Free tiers that never expire, on any account — a lab like the one below uses a rounding error's worth of each. API Gateway's HTTP APIs get 1 million free calls a month for 12 months on legacy accounts, or draw a negligible amount from the $200 credit on newer ones. The one thing worth remembering to clean up: an EventBridge scheduled rule left running keeps firing forever, quietly accumulating Lambda invocations (still free at this scale, but not something to leave running indefinitely by accident).

Hands-On: A Real Event-Driven Order Pipeline

This lab builds one coherent system: an HTTP endpoint receives an "order," publishes it to SNS, which fans out to two separate SQS queues, each processed by its own independent Lambda — plus a fourth Lambda running on an EventBridge schedule, completely separate from the order flow. All five services from this week, wired together and actually working.

Step 1 — A shared Lambda execution role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat > lambda-trust-policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}
]
}
JSON

aws iam create-role --role-name ZeroToHeroLambdaRole \
--assume-role-policy-document file://lambda-trust-policy.json

aws iam attach-role-policy --role-name ZeroToHeroLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam attach-role-policy --role-name ZeroToHeroLambdaRole \
--policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess
aws iam attach-role-policy --role-name ZeroToHeroLambdaRole \
--policy-arn arn:aws:iam::aws:policy/AmazonSNSFullAccess

ROLE_ARN=$(aws iam get-role --role-name ZeroToHeroLambdaRole --query 'Role.Arn' --output text)
sleep 10 # let IAM finish propagating before Lambda tries to use this role
(Using broad managed policies here purely to keep the lab short — Week 3's least-privilege lesson still applies for anything real: scope these down to the specific queue and topic ARNs in production.)

Step 2 — Two SQS queues

1
2
3
4
5
6
7
EMAIL_QUEUE_URL=$(aws sqs create-queue --queue-name zero-to-hero-email-queue --query 'QueueUrl' --output text)
EMAIL_QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url $EMAIL_QUEUE_URL \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

INV_QUEUE_URL=$(aws sqs create-queue --queue-name zero-to-hero-inventory-queue --query 'QueueUrl' --output text)
INV_QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url $INV_QUEUE_URL \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

Step 3 — An SNS topic, fanning out into both queues

1
2
3
4
TOPIC_ARN=$(aws sns create-topic --name zero-to-hero-order-events --query 'TopicArn' --output text)

aws sns subscribe --topic-arn $TOPIC_ARN --protocol sqs --notification-endpoint $EMAIL_QUEUE_ARN
aws sns subscribe --topic-arn $TOPIC_ARN --protocol sqs --notification-endpoint $INV_QUEUE_ARN
A real gotcha worth knowing about: SNS can't actually deliver into an SQS queue until the queue's own policy explicitly allows it — a resource-based policy, the same underlying idea as Week 3's IAM policies, just attached to the queue instead of a user:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for Q in "EMAIL:$EMAIL_QUEUE_URL:$EMAIL_QUEUE_ARN" "INV:$INV_QUEUE_URL:$INV_QUEUE_ARN"; do
IFS=":" read NAME URL ARN <<< "$Q"
cat > sqs-policy-$NAME.json <<JSON
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "sns.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "$ARN",
"Condition": {"ArnEquals": {"aws:SourceArn": "$TOPIC_ARN"}}
}]
}
JSON

aws sqs set-queue-attributes --queue-url $URL \
--attributes Policy="$(cat sqs-policy-$NAME.json)"
done

Step 4 — Write and package the four Lambda functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mkdir -p lambda-build && cd lambda-build

cat > email_processor.py <<'PY'
import json
def handler(event, context):
for record in event['Records']:
body = json.loads(json.loads(record['body'])['Message'])
print(f"[EMAIL] Would send confirmation for order {body.get('order_id')}")
return {"statusCode": 200}
PY
zip email_processor.zip email_processor.py

cat > inventory_processor.py <<'PY'
import json
def handler(event, context):
for record in event['Records']:
body = json.loads(json.loads(record['body'])['Message'])
print(f"[INVENTORY] Would decrement stock for order {body.get('order_id')}")
return {"statusCode": 200}
PY
zip inventory_processor.zip inventory_processor.py

cat > api_handler.py <<'PY'
import json, boto3, os
sns = boto3.client('sns')
def handler(event, context):
body = json.loads(event.get('body', '{}'))
sns.publish(TopicArn=os.environ['TOPIC_ARN'], Message=json.dumps(body))
return {"statusCode": 200, "body": json.dumps({"status": "order received"})}
PY
zip api_handler.zip api_handler.py

cat > scheduled_report.py <<'PY'
def handler(event, context):
print("[SCHEDULED] Report job ran — triggered by EventBridge, no queue involved.")
return {"statusCode": 200}
PY
zip scheduled_report.zip scheduled_report.py

Step 5 — Deploy all four

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
aws lambda create-function --function-name zero-to-hero-email-processor \
--runtime python3.13 --role $ROLE_ARN --handler email_processor.handler \
--zip-file fileb://email_processor.zip --timeout 10

aws lambda create-function --function-name zero-to-hero-inventory-processor \
--runtime python3.13 --role $ROLE_ARN --handler inventory_processor.handler \
--zip-file fileb://inventory_processor.zip --timeout 10

aws lambda create-function --function-name zero-to-hero-api-handler \
--runtime python3.13 --role $ROLE_ARN --handler api_handler.handler \
--zip-file fileb://api_handler.zip --timeout 10 \
--environment "Variables={TOPIC_ARN=$TOPIC_ARN}"

aws lambda create-function --function-name zero-to-hero-scheduled-report \
--runtime python3.13 --role $ROLE_ARN --handler scheduled_report.handler \
--zip-file fileb://scheduled_report.zip --timeout 10

Step 6 — Wire each SQS queue to its own Lambda

1
2
3
4
5
6
7
aws lambda create-event-source-mapping \
--function-name zero-to-hero-email-processor \
--event-source-arn $EMAIL_QUEUE_ARN --batch-size 1

aws lambda create-event-source-mapping \
--function-name zero-to-hero-inventory-processor \
--event-source-arn $INV_QUEUE_ARN --batch-size 1

Step 7 — An HTTP API in front of the entry-point Lambda

1
2
3
4
5
6
7
8
9
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

API_ID=$(aws apigatewayv2 create-api \
--name zero-to-hero-api --protocol-type HTTP \
--target arn:aws:lambda:<your-region>:$ACCOUNT_ID:function:zero-to-hero-api-handler \
--query 'ApiId' --output text)

API_ENDPOINT=$(aws apigatewayv2 get-api --api-id $API_ID --query 'ApiEndpoint' --output text)
echo $API_ENDPOINT
The --target shorthand quick-creates a default route, integration, and the Lambda invoke permission all in one call — the fastest legitimate way to stand up a single-function HTTP API.

Step 8 — Fire a real order through the whole pipeline

1
2
3
curl -X POST $API_ENDPOINT/ \
-H "Content-Type: application/json" \
-d '{"order_id": "ZTH-001"}'
Then check both downstream Lambdas actually fired independently:
1
2
aws logs tail /aws/lambda/zero-to-hero-email-processor --since 2m
aws logs tail /aws/lambda/zero-to-hero-inventory-processor --since 2m
Both should show ZTH-001 logged — one HTTP request, one SNS publish, two completely independent Lambda invocations that never called each other directly. That's fan-out, working for real.

Step 9 — EventBridge, on a schedule, entirely separate from the order flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
aws events put-rule \
--name zero-to-hero-schedule \
--schedule-expression "rate(5 minutes)"

SCHEDULED_LAMBDA_ARN=$(aws lambda get-function \
--function-name zero-to-hero-scheduled-report \
--query 'Configuration.FunctionArn' --output text)

aws lambda add-permission \
--function-name zero-to-hero-scheduled-report \
--statement-id eventbridge-invoke \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn arn:aws:events:<your-region>:$ACCOUNT_ID:rule/zero-to-hero-schedule

aws events put-targets --rule zero-to-hero-schedule \
--targets "Id"="1","Arn"="$SCHEDULED_LAMBDA_ARN"
Wait five minutes, then check:
1
aws logs tail /aws/lambda/zero-to-hero-scheduled-report --since 6m

Cleanup — the EventBridge rule matters most, since it runs forever otherwise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1. Stop the recurring schedule first
aws events remove-targets --rule zero-to-hero-schedule --ids 1
aws events delete-rule --name zero-to-hero-schedule

# 2. API Gateway
aws apigatewayv2 delete-api --api-id $API_ID

# 3. Event source mappings
UUID1=$(aws lambda list-event-source-mappings \
--function-name zero-to-hero-email-processor --query 'EventSourceMappings[0].UUID' --output text)
aws lambda delete-event-source-mapping --uuid $UUID1

UUID2=$(aws lambda list-event-source-mappings \
--function-name zero-to-hero-inventory-processor --query 'EventSourceMappings[0].UUID' --output text)
aws lambda delete-event-source-mapping --uuid $UUID2

# 4. Lambda functions
for FN in zero-to-hero-api-handler zero-to-hero-email-processor zero-to-hero-inventory-processor zero-to-hero-scheduled-report; do
aws lambda delete-function --function-name $FN
done

# 5. SNS subscriptions, then the topic
for SUB in $(aws sns list-subscriptions-by-topic --topic-arn $TOPIC_ARN --query 'Subscriptions[].SubscriptionArn' --output text); do
aws sns unsubscribe --subscription-arn $SUB
done
aws sns delete-topic --topic-arn $TOPIC_ARN

# 6. SQS queues
aws sqs delete-queue --queue-url $EMAIL_QUEUE_URL
aws sqs delete-queue --queue-url $INV_QUEUE_URL

# 7. IAM role
aws iam detach-role-policy --role-name ZeroToHeroLambdaRole --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam detach-role-policy --role-name ZeroToHeroLambdaRole --policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess
aws iam detach-role-policy --role-name ZeroToHeroLambdaRole --policy-arn arn:aws:iam::aws:policy/AmazonSNSFullAccess
aws iam delete-role --role-name ZeroToHeroLambdaRole

My Take

This week helped me see how Lambda, SNS, SQS, and S3 can work together without needing everything to run on an EC2 server. The interesting part is how these services communicate through events, making applications more scalable and loosely coupled with less infrastructure to manage.

Next week — Part 8: Containers on AWS — ECS, EKS, and Fargate, and when you actually outgrow everything built this week.

Series: AWS: Zero to Hero (9 articles)

  1. …
  2. 7
    AWS: Zero to Hero — Week 7: Serverless & Event-Driven AWS — Lambda, API Gateway, SQS, SNS, EventBridge This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article