Exam Guide: Developer - Associate
🏗️ Domain 4: Troubleshooting And Optimization
📘 Task 3: Optimize Applications By Using AWS Services And Features
This task is about making applications faster, cheaper, and more efficient. You need to understand Lambda concurrency and memory tuning, caching at every layer (CloudFront, API Gateway, ElastiCache), messaging optimization with SNS filter policies, and how to read metrics to find bottlenecks. Choose the right optimization for a given symptom such as high latency, throttling, slow stream processing, or runaway cost, and etc.
📘 Concepts
Lambda Concurrency Types
| Type | What It Does | Cost | Use Case |
|---|---|---|---|
| Unreserved | Shared pool (default 1,000 per region) | Pay per invocation | Default for most functions |
| Reserved | Guarantees capacity AND caps the function | No extra cost | Protect downstream services, guarantee capacity |
| Provisioned | Pre-warms execution environments | Pay even when idle | Eliminate cold starts for latency-sensitive functions |
💡 Reserved concurrency does double duty. It guarantees a function gets that many concurrent executions AND prevents it from exceeding that number. Use it to stop a busy function from starving others, or to protect a downstream database from too many connections. Provisioned concurrency is the only thing that eliminates cold starts, but you pay for it 24/7.
The Concurrency Formula
Concurrent executions = (invocations per second) × (average duration in seconds)
Example: 100 requests/second × 0.5 seconds = 50 concurrent executions needed.
Memory and CPU Relationship
Lambda allocates CPU proportionally to memory. At 1,769 MB you get one full vCPU. More memory means more CPU, which can make a function run faster. Sometimes fast enough that the higher per-ms cost is offset by the shorter duration.
Memory |
Relative CPU | Typical Result |
|---|---|---|
128 MB |
Fraction of a vCPU | Cheapest per-ms, slowest |
512 MB |
~0.3 vCPU | Often the sweet spot |
1,769 MB |
1 full vCPU | Fast, higher per-ms cost |
10,240 MB |
~6 vCPUs | Fastest, for CPU-bound work |
Caching Layers
| Layer | Service | What It Caches | TTL Control |
|---|---|---|---|
| Edge | CloudFront | Static content, API responses | Cache policies, headers |
| API | API Gateway cache | Endpoint responses | Per-stage, per-method |
| Application | ElastiCache (Redis/Memcached) | Query results, sessions | Set in code |
| Database | DAX (DynamoDB only) | DynamoDB reads | Item + query cache TTL |
Caching Patterns
| Pattern | How It Works | Best For |
|---|---|---|
| Cache-aside (lazy loading) | Check cache → miss → fetch from DB → store | Read-heavy, tolerates some staleness |
| Write-through | Write to cache AND DB simultaneously | Consistency-sensitive reads |
| Write-behind | Write to cache, async write to DB | High write throughput |
| TTL-based | Entries expire after a set time | Most general-purpose caching |
💡 Cache-aside is the most common pattern. The risk is a cache stampede (many simultaneous misses hitting the DB). Write-through keeps the cache fresh but adds write latency.
CloudFront Cache Keys
The cache key determines what makes a request unique. The fewer components in the key, the higher the cache hit rate.
| Cache Key Component | Effect on Hit Rate |
|---|---|
| No headers or query strings | Highest hit rate |
| Whitelist only what matters | Balanced |
| Forward everything | Lowest hit rate (every request unique) |
SNS Subscription Filter Policies
Filter policies let each subscriber receive only the messages it cares about, reducing downstream processing and cost.
| Filter Scope | Filters On |
|---|---|
| MessageAttributes (default) | Message attribute key/value pairs |
| MessageBody | Fields inside the message body JSON |
Key Metrics for Finding Bottlenecks
Metric |
Service | What It Signals |
|---|---|---|
Duration |
Lambda | Slow function or slow downstream call |
IteratorAge |
Lambda (Kinesis/DynamoDB) | Consumer falling behind the stream |
ApproximateAgeOfOldestMessage |
SQS | Consumer too slow, queue backing up |
Throttles |
Lambda | Hitting concurrency limits |
ConsumedReadCapacityUnits |
DynamoDB | Hot partition or under-provisioning |
CacheHitRate |
CloudFront / API Gateway | Cache effectiveness |
🏗️ Build A Performance Optimization Lab
Build a Performance Optimization Lab using the AWS Console:
- A Lambda function tuned across memory settings to find the cost/performance sweet spot
- Reserved concurrency configured to protect a downstream service
- API Gateway caching enabled and tested for cache hits
- An SNS topic with subscription filter policies routing messages selectively
- CloudWatch metrics queries to identify bottlenecks
Prerequisites
Part I
Tune Lambda Memory for Cost and Performance
Create a CPU-Bound Function
1 Open the Lambda console → Create function
-
Function name:
MemoryTuningDemo - Runtime: Python 3.13
2 Click Create function
3 Paste this CPU-bound code
import json
import time
def lambda_handler(event, context):
"""
A CPU-bound function that computes prime numbers.
CPU-bound work benefits directly from more memory (= more CPU),
making it ideal for demonstrating memory tuning.
"""
start = time.time()
limit = event.get('limit', 500000)
primes = []
for num in range(2, limit):
is_prime = True
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
primes.append(num)
duration_ms = (time.time() - start) * 1000
return {
'statusCode': 200,
'body': json.dumps({
'primesFound': len(primes),
'durationMs': round(duration_ms, 1),
'memoryConfigured': context.memory_limit_in_mb
})
}
4 Click Deploy
Run the Memory Tuning Experiment
5 Go to Configuration → General configuration → Edit → set Timeout to 1 min 0 sec → Save (so the function doesn't time out at low memory)
6 Create a test event named PrimeTest with {}
7 Now run the experiment. For each memory setting below, go to Configuration → General configuration → Edit → change Memory → Save, then run the test and record the results.
You'll see duration drop sharply as memory increases, then flatten out. The point where it flattens is your sweet spot. More memory past that just costs more without speeding things up.
💡 For CPU-bound work, increasing memory often reduces total cost because the function finishes much faster. The AWS Lambda Power Tuning tool (a Step Functions state machine) automates this experiment and produces a cost/performance graph. Memory and CPU scale together.
Part II
Use Reserved Concurrency to Protect Downstream Services
The Scenario
Imagine MemoryTuningDemo calls a downstream database that can only handle 10 simultaneous connections. If Lambda scales to 100 concurrent executions, it overwhelms the database. Reserved concurrency caps this.
Configure Reserved Concurrency
1 On MemoryTuningDemo, go to Configuration → Concurrency and recursion detection → Concurrency → Edit
2 Select Reserve concurrency
3 Set Reserved concurrency to 10
4 Click Save
💡 Now this function can never have more than 10 concurrent executions. Any invocations beyond that are throttled (for async sources) or receive a 429 (for synchronous sources).
Observe Throttling
5 Open CloudShell and run a burst of async invocations
for i in $(seq 1 50); do
aws lambda invoke \
--function-name MemoryTuningDemo \
--invocation-type Event \
--payload '{"limit": 1000000}' \
/dev/null &
done
wait
6 Open the CloudWatch console → Metrics → Lambda → By Function Name → MemoryTuningDemo
7 Add the Throttles and ConcurrentExecutions metrics
8 You'll see ConcurrentExecutions cap at 10 and Throttles climb as excess invocations are rejected
💡 Reserved concurrency is free and acts as both a guarantee and a ceiling. It's the answer when a question situation describes a Lambda function overwhelming a downstream resource (RDS connections, a third-party API rate limit). For throttled async invocations, the events are retried. For synchronous, the caller gets a 429.
Part III
Enable and Test API Gateway Caching
Create an API
1 Open the API Gateway console → Create API → REST API → Build
2 API name: CachingDemoAPI → Create API
3. Create resource → Resource name: products → Create resource
4 Select /products → Create method → GET → Lambda Function (proxy) → select MemoryTuningDemo → Create method
Deploy to a Stage
5 Deploy API → New stage → Stage name: prod → Deploy
Enable Caching on the Stage
6 In the left sidebar, click Stages → select prod
7 Under Stage details, click Edit
8 Enable Cache settings:
- Provision API cache: Enabled
- Cache capacity: 0.5 GB
9 Click Save changes (the cache takes a few minutes to provision)
Enable Caching on the Method
10 With the prod stage selected, expand the resource tree and click the GET method under /products
11 Under Method settings, edit:
- Enable method cache: checked
- Cache TTL: 300 seconds
12 Save
Test Cache Behaviour
13 Copy the stage Invoke URL and call the endpoint a few times:
# First call — cache miss (slower, hits Lambda)
curl -w "\nTime: %{time_total}s\n" https://YOUR_API/prod/products
# Subsequent calls — cache hit (faster, served from cache)
curl -w "\nTime: %{time_total}s\n" https://YOUR_API/prod/products
curl -w "\nTime: %{time_total}s\n" https://YOUR_API/prod/products
14 The first call is slower (cache miss → Lambda runs). Subsequent calls within the TTL are much faster (served from cache, Lambda not invoked).
15 Check CloudWatch → API Gateway metrics → CacheHitCount and CacheMissCount
💡 API Gateway caching is configured per-stage and per-method. The cache key is based on the request parameters you specify. Caching reduces backend load and latency but can serve stale data within the TTL. You can force a cache bypass with the
Cache-Control: max-age=0header (if you enable that option).
Part IV
Optimize Messaging with SNS Filter Policies
Create an SNS Topic
1 Open the SNS console → Topics → Create topic
2 Type: Standard
3 Name: order-events
4 Click Create topic
Create Two SQS Queues
5 Open the SQS console → Create queue
-
Name:
premium-orders→ Create queue 6 Create another: -
Name:
all-orders→ Create queue
Subscribe the Queues with Filter Policies
7 Back in SNS → order-events topic → Create subscription
8 For the premium queue:
- Protocol: Amazon SQS
-
Endpoint: select the
premium-ordersqueue ARN - Expand Subscription filter policy
- Filter policy scope: Message attributes
- Policy:
{
"orderType": ["premium"],
"orderTotal": [{"numeric": [">=", 100]}]
}
- Click Create subscription
9 Create another subscription for all-orders with no filter policy (it receives everything)
Test the Filtering
10 On the order-events topic, click Publish message
11 Publish a premium order:
-
Message body:
{"orderId": "ORD-001", "amount": 250} - Under Message attributes, add:
- Name:
orderType, Type: String, Value:premium - Name:
orderTotal, Type: Number, Value:250
- Name:
- Click Publish message
12 Publish a standard order:
-
Message body:
{"orderId": "ORD-002", "amount": 30} - Message attributes:
- Name:
orderType, Type: String, Value:standard - Name:
orderTotal, Type: Number, Value:30
- Name:
- Click Publish message
Verify the Results
13 Open SQS → all-orders → Send and receive messages → Poll for messages
- Both orders appear (no filter)
14 Open SQS → premium-orders → Poll for messages
- Only the premium order ($250) appears. The standard order was filtered out
💡 Filter policies are evaluated by SNS before delivery, so filtered-out messages never reach the subscriber thus saving SQS, Lambda, and processing costs. Without filter policies, every subscriber receives every message and must filter in code (wasteful). The default scope filters on message attributes; set the scope to MessageBody to filter on body content instead.
Part V
Identify Bottlenecks with CloudWatch Metrics
Common Bottleneck Patterns
| Symptom | Likely Bottleneck | Fix |
|---|---|---|
| High Lambda Duration | Slow downstream call | Cache the result, check X-Ray traces |
| High IteratorAge | Lambda can't keep up with the stream | Increase parallelization factor, add shards |
SQS ApproximateAgeOfOldestMessage growing |
Consumer too slow | Add consumers, increase batch size |
| DynamoDB throttling | Hot partition or low capacity | Redesign keys, switch to on-demand |
| API Gateway 429 | Throttle limit reached | Raise limits, add caching |
| High memory usage | Memory leak or large payloads | Profile code, stream large data |
Query Lambda Performance with Logs Insights
1 Open CloudWatch → Logs → Logs Insights
2 Select /aws/lambda/MemoryTuningDemo
3 Run this query to spot memory over-provisioning:
filter @type = "REPORT"
| stats avg(@maxMemoryUsed/1000/1000) as avgMemMB,
max(@maxMemoryUsed/1000/1000) as peakMemMB,
max(@memorySize/1000/1000) as configuredMB,
avg(@duration) as avgDurationMs,
pct(@duration, 95) as p95Ms,
pct(@duration, 99) as p99Ms
4 If peakMemMB is far below configuredMB, you're over-provisioned on memory. If p99Ms is much higher than avgDurationMs, you have tail latency worth investigating with X-Ray.
Using X-Ray to Pinpoint the Slow Call
A trace makes the bottleneck obvious.
API Gateway: 5ms
└── Lambda: 2500ms
├── DynamoDB GetItem: 15ms
├── External API call: 2200ms ← bottleneck
└── SQS SendMessage: 8ms
The external API is the problem. Fixes: cache the response, make the call asynchronous (send to SQS), or add a circuit breaker to fail fast.
💡 Match the symptom to the metric. "Stream processing is delayed" → IteratorAge. "Queue is backing up" → ApproximateAgeOfOldestMessage. "Which downstream call is slow?" → X-Ray trace.
🏗️ What You Built | 📘 Exam Concepts Recap
| What You Built | Exam Concept |
|---|---|
| Ran a memory-tuning experiment across settings | Memory = CPU. finding the cost/performance sweet spot |
| Set reserved concurrency to 10 and observed throttles | Reserved concurrency as guarantee + ceiling |
| Burst-invoked to trigger throttling | ConcurrentExecutions and Throttles metrics |
| Enabled API Gateway caching per stage and method | Reducing backend load and latency with caching |
| Measured cache hits vs misses with curl timing | Cache TTL behavior and CacheHitCount metric |
| Created SNS subscriptions with filter policies | Selective message delivery to reduce cost |
| Published messages with matching/non-matching attributes | MessageAttributes filter scope |
Queried @maxMemoryUsed and percentiles in Logs Insights |
Detecting over-provisioning and tail latency |
| Mapped symptoms to metrics (IteratorAge, etc.) | Choosing the right diagnostic for a bottleneck |
⚠️ Clean Up Protocol
-
API Gateway → Delete
CachingDemoAPI(this also removes the provisioned cache) -
Lambda → Delete
MemoryTuningDemo -
SNS → Delete the
order-eventstopic (removes subscriptions) -
SQS → Delete
premium-ordersandall-ordersqueues - IAM → Delete the Lambda execution role
- CloudWatch → Delete the log group for the function
Key Takeaways
- Reserved concurrency = guarantee + throttle (free). Provisioned concurrency = no cold starts (paid even when idle).
- Memory = CPU. For CPU-bound work, more memory can lower total cost by finishing faster. Lambda Power Tuning finds the sweet spot.
- Cache-aside is the most common caching pattern. Check cache → miss → fetch → store.
- CloudFront cache keys: fewer components = higher hit rate. Whitelist only what affects the response.
- API Gateway caching is per-stage and per-method, with a configurable TTL.
- SNS filter policies stop unwanted messages before delivery saving downstream cost. Default scope is message attributes.
- IteratorAge = stream consumer falling behind.
- ApproximateAgeOfOldestMessage = SQS queue backing up.
- X-Ray traces are the fastest way to find which downstream call is the bottleneck.
- Match the symptom to the metric.
Additional Resources
- Understanding Lambda function scaling
- Configuring provisioned concurrency for a function
- Cache settings for REST APIs in API Gateway
- Amazon SNS message filtering
- Caching strategies for Memcached
- AWS Lambda Power Tuning
🏗️
Top comments (0)