DEV Community

Cover image for Optimize Applications By Using AWS Services And Features | 🏗️ Build A Performance Optimisation Lab

Optimize Applications By Using AWS Services And Features | 🏗️ Build A Performance Optimisation Lab

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)
Enter fullscreen mode Exit fullscreen mode

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
        })
    }
Enter fullscreen mode Exit fullscreen mode

4 Click Deploy

Run the Memory Tuning Experiment

5 Go to ConfigurationGeneral configurationEdit → set Timeout to 1 min 0 secSave (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 ConfigurationGeneral configurationEdit → change MemorySave, 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 ConfigurationConcurrency and recursion detectionConcurrencyEdit

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
Enter fullscreen mode Exit fullscreen mode

6 Open the CloudWatch console → MetricsLambdaBy Function NameMemoryTuningDemo

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 APIREST APIBuild

2 API name: CachingDemoAPICreate API

3. Create resource → Resource name: productsCreate resource

4 Select /productsCreate methodGETLambda Function (proxy) → select MemoryTuningDemoCreate method

Deploy to a Stage

5 Deploy APINew stage → Stage name: prodDeploy

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
Enter fullscreen mode Exit fullscreen mode

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=0 header (if you enable that option).


Part IV

Optimize Messaging with SNS Filter Policies

Create an SNS Topic

1 Open the SNS console → TopicsCreate 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-ordersCreate queue 6 Create another:
  • Name: all-ordersCreate queue

Subscribe the Queues with Filter Policies

7 Back in SNSorder-events topic → Create subscription

8 For the premium queue:

  • Protocol: Amazon SQS
  • Endpoint: select the premium-orders queue ARN
  • Expand Subscription filter policy
  • Filter policy scope: Message attributes
  • Policy:
{
  "orderType": ["premium"],
  "orderTotal": [{"numeric": [">=", 100]}]
}
Enter fullscreen mode Exit fullscreen mode
  • 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
  • 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
  • Click Publish message

Verify the Results

13 Open SQSall-ordersSend and receive messagesPoll for messages

  • Both orders appear (no filter)

14 Open SQSpremium-ordersPoll 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 CloudWatchLogsLogs 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. API Gateway → Delete CachingDemoAPI (this also removes the provisioned cache)
  2. Lambda → Delete MemoryTuningDemo
  3. SNS → Delete the order-events topic (removes subscriptions)
  4. SQS → Delete premium-orders and all-orders queues
  5. IAM → Delete the Lambda execution role
  6. CloudWatch → Delete the log group for the function

Key Takeaways

  1. Reserved concurrency = guarantee + throttle (free). Provisioned concurrency = no cold starts (paid even when idle).
  2. Memory = CPU. For CPU-bound work, more memory can lower total cost by finishing faster. Lambda Power Tuning finds the sweet spot.
  3. Cache-aside is the most common caching pattern. Check cache → miss → fetch → store.
  4. CloudFront cache keys: fewer components = higher hit rate. Whitelist only what affects the response.
  5. API Gateway caching is per-stage and per-method, with a configurable TTL.
  6. SNS filter policies stop unwanted messages before delivery saving downstream cost. Default scope is message attributes.
  7. IteratorAge = stream consumer falling behind.
  8. ApproximateAgeOfOldestMessage = SQS queue backing up.
  9. X-Ray traces are the fastest way to find which downstream call is the bottleneck.
  10. Match the symptom to the metric.

Additional Resources


🏗️

Top comments (0)