AWS Well-Architected Framework
7 Performance Quick Wins That Make Your AWS Application Feel 2× Faster
Slow application performance is killing conversions. These 7 AWS optimisations can cut latency in half and improve user experience—most take under an hour to implement.
Cloud Associates
I recently worked with a SaaS application that felt painfully slow—p95 latency was sitting at 2.1 seconds, and users were bouncing before pages even loaded. The team was frustrated watching potential customers abandon their signup flow.
Over 3 days, I implemented 7 performance optimisations that dropped their p95 latency to 580ms (73% improvement) and increased conversion rate by 12%.
The best part? None of these required rewrites or complex engineering. They were all straightforward AWS configuration changes and web performance best practices.
These are the 7 highest-impact performance improvements I’ve found consistently deliver results—the ones that make users say “wow, this feels so much faster.”
Understanding Performance Impact
Before we dive into solutions, let’s understand how latency affects your business:
Amazon found: 100ms of added latency costs 1% of sales
Google found: 500ms slower = 20% drop in traffic
Akamai found: 2-second delay = 100% increase in bounce rate
Your startup:
- p95 latency >1 second = bleeding customers
- p95 latency <500ms = competitive advantage
- p95 latency <200ms = feels instant
My goal: Get you under 500ms p95 with quick wins, then optimize further.
Quick Win #1: Enable CloudFront Compression (5 Minutes, 60-70% Bandwidth Reduction)
The Problem
What I typically see:
- JavaScript bundle: 1.2 MB uncompressed
- CSS files: 340 KB uncompressed
- JSON API responses: 85 KB uncompressed
- None of it compressed
User impact:
- On 4G connection: 1.2 MB file takes 2.4 seconds to download
- Compressed to 350 KB: Takes 0.7 seconds
- Users wait 1.7 seconds less per page load
The Solution
Enable automatic compression in CloudFront (literally one checkbox).
Implementation
# Enable compression on CloudFront distribution
aws cloudfront update-distribution \
--id E1234EXAMPLE \
--distribution-config file://distribution-config.json
distribution-config.json (key settings):
{
"DefaultCacheBehavior": {
"Compress": true,
"TargetOriginId": "S3-my-bucket",
"ViewerProtocolPolicy": "redirect-to-https"
}
}
That’s it. CloudFront will now automatically compress text-based responses (HTML, CSS, JavaScript, JSON, XML, SVG).
What Gets Compressed
| File Type | Before | After | Reduction |
|---|---|---|---|
| JavaScript | 1.2 MB | 350 KB | 71% |
| CSS | 340 KB | 95 KB | 72% |
| HTML | 120 KB | 30 KB | 75% |
| JSON API | 85 KB | 22 KB | 74% |
Images, videos, PDFs don’t benefit (already compressed)
Impact
- Page load time: 3.2s → 1.1s (65% faster)
- Bandwidth costs: -70%
- Time to implement: 5 minutes
How to Verify
# Test compression is working
curl -H "Accept-Encoding: gzip" https://d123456.cloudfront.net/app.js -I | grep content-encoding
# Expected output:
content-encoding: gzip
Verdict: This is the single highest-impact, lowest-effort performance optimization. Do it first.
Quick Win #2: Add CloudFront in Front of S3 Static Assets (1 Hour, 50-70% Latency Reduction)
The Problem
What I typically see:
- Static assets served directly from S3:
https://my-bucket.s3.amazonaws.com/images/logo.png - User in Sydney, S3 bucket in us-east-1
- Latency: 280ms just for the roundtrip (before downloading file)
- Same asset requested on every page load (no caching)
User impact:
- Logo loads in 280ms + download time
- 10 images per page × 280ms = 2.8 seconds extra
The Solution
Serve static assets through CloudFront (global CDN with edge caching).
Implementation
Step 1: Create CloudFront distribution for S3 bucket
aws cloudfront create-distribution --origin-domain-name my-bucket.s3.ap-southeast-2.amazonaws.com
Step 2: Configure S3 bucket policy to allow CloudFront
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789:distribution/E1234EXAMPLE"
}
}
}
]
}
Step 3: Update application to use CloudFront URLs
// Before
const imageUrl = `https://my-bucket.s3.amazonaws.com/images/${image}.jpg`;
// After
const imageUrl = `https://d123456.cloudfront.net/images/${image}.jpg`;
How CloudFront Improves Performance
First request (cache miss):
- User in Sydney requests logo
- CloudFront edge in Sydney doesn’t have it cached
- Edge fetches from S3 us-east-1 (280ms)
- Edge caches and serves to user
- Total: 280ms + download time
Subsequent requests (cache hit):
- User in Sydney requests logo
- CloudFront edge in Sydney has it cached
- Served directly from Sydney edge (5-15ms)
- Total: 15ms + download time
Impact
- First load: 280ms → 280ms (same, cache miss)
- Repeat visits: 280ms → 15ms (95% faster)
- Cache hit ratio: Typically 90-95%
- Average latency improvement: 50-70%
Bonus: Set Long Cache TTLs
{
"DefaultCacheBehavior": {
"MinTTL": 86400,
"MaxTTL": 31536000,
"DefaultTTL": 86400
}
}
This caches assets for 24 hours minimum, 1 year maximum.
Important: Use versioned filenames for cache busting:
app.abc123.jsinstead ofapp.js- When you deploy new code, filename changes, cache is bypassed
Impact
- Static asset latency: 280ms → 15ms (95% improvement)
- Page load time: -30% to -50%
- Time to implement: 1 hour
Quick Win #3: Implement ElastiCache for Database Query Caching (2-3 Hours, 80-95% Faster for Cached Queries)
The Problem
What I typically see:
- Same database query executed 1,000 times/minute
- Query takes 50ms
- Total database time: 50 seconds/minute just for this query
- Database CPU at 60% doing redundant work
Example: Homepage shows “Top 10 Products” which changes once per hour, but query runs on every page load.
The Solution
Cache query results in Redis (ElastiCache) for fast retrieval.
Implementation
Step 1: Launch ElastiCache Redis cluster
aws elasticache create-cache-cluster \
--cache-cluster-id production-redis \
--cache-node-type cache.t3.micro \
--engine redis \
--num-cache-nodes 1 \
--security-group-ids sg-123456
Step 2: Install Redis client in application
npm install redis
Step 3: Implement caching layer
const redis = require('redis');
const client = redis.createClient({
host: 'production-redis.abc123.cache.amazonaws.com',
port: 6379
});
// Before (every request hits database)
app.get('/api/top-products', async (req, res) => {
const products = await db.query('SELECT * FROM products ORDER BY sales DESC LIMIT 10');
res.json(products.rows);
});
// After (cache for 1 hour)
app.get('/api/top-products', async (req, res) => {
const cacheKey = 'top-products';
// Try to get from cache first
const cached = await client.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Cache miss - query database
const products = await db.query('SELECT * FROM products ORDER BY sales DESC LIMIT 10');
// Store in cache for 1 hour
await client.setEx(cacheKey, 3600, JSON.stringify(products.rows));
res.json(products.rows);
});
Impact
First request (cache miss):
- Query database: 50ms
- Store in Redis: 2ms
- Total: 52ms
Subsequent requests (cache hit):
- Get from Redis: 2ms
- Total: 2ms
Result: 50ms → 2ms (96% faster)
What to Cache
Good candidates:
- Frequently accessed, rarely changing data (product catalogs, blog posts)
- Expensive computations (recommendation algorithms, analytics aggregations)
- API responses from third-party services
Don’t cache:
- User-specific data (unless keyed by user ID)
- Real-time data (stock prices, live scores)
- Data that changes on every request
Cache Invalidation Strategy
// When product is updated, invalidate cache
app.post('/admin/products/:id', async (req, res) => {
await db.query('UPDATE products SET name = $1 WHERE id = $2', [req.body.name, req.params.id]);
// Invalidate relevant caches
await client.del('top-products');
await client.del(`product:${req.params.id}`);
res.json({ success: true });
});
Cost
- cache.t3.micro: $12/month
- Saves database CPU: -$30-50/month (can downsize database)
- Net cost: $0 to -$30/month
Impact
- Cached query latency: 50ms → 2ms (96% faster)
- Database CPU: -40%
- API response time: -30-50%
- Time to implement: 2-3 hours
Quick Win #4: Enable HTTP/2 on CloudFront (2 Minutes, 30% Faster Page Loads)
The Problem
What I typically see:
- CloudFront distribution using HTTP/1.1
- Browser makes 15 requests for a page (HTML, CSS, 5× JS, 8× images)
- HTTP/1.1 limits: 6 parallel connections per domain
- Requests queue and wait
User impact:
- First 6 requests: Start immediately
- Next 6 requests: Wait for first batch to finish
- Next 3 requests: Wait for second batch
- Total time: 3× longer than necessary
The Solution
Enable HTTP/2 on CloudFront (supports unlimited parallel requests).
Implementation
# Enable HTTP/2 on CloudFront distribution
aws cloudfront update-distribution \
--id E1234EXAMPLE \
--distribution-config file://http2-config.json
http2-config.json:
{
"HttpVersion": "http2"
}
That’s it. Literally one setting.
How HTTP/2 Improves Performance
HTTP/1.1:
- 15 requests need to load
- 6 at a time (browser limit)
- 3 rounds of requests
- If each request takes 200ms: 600ms total
HTTP/2:
- 15 requests load in parallel (multiplexing)
- All start immediately
- If each request takes 200ms: 200ms total
Result: 600ms → 200ms (67% faster)
Impact
- Page load time: -30%
- Number of roundtrips: -67%
- Time to implement: 2 minutes
Quick Win #5: Optimize Images with Automatic Formatting (1 Hour, 25-50% Smaller Images)
The Problem
What I typically see:
- Product images: JPEG, 2400×1800 pixels, 1.2 MB each
- Displayed on screen: 600×450 pixels
- Sending 4× more pixels than needed
- Not using modern formats (WebP is 25-35% smaller than JPEG)
User impact:
- 10 product images = 12 MB download
- On 4G: 24 seconds to load all images
- Users bouncing before images load
The Solution
Use Lambda@Edge to automatically resize and convert images to WebP.
Implementation
Step 1: Create Lambda@Edge function for image optimization
// This runs at CloudFront edge on every request
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
// Check if browser supports WebP
const headers = request.headers;
const acceptHeader = headers['accept'] ? headers['accept'][0].value : '';
const supportsWebP = acceptHeader.includes('image/webp');
// Extract desired width from query string
// Example: /images/product.jpg?w=600
const params = new URLSearchParams(request.querystring);
const width = params.get('w') || 'original';
// Modify S3 key to request resized WebP version
if (supportsWebP && width !== 'original') {
request.uri = `/resized/${width}/webp${uri}`;
}
return request;
};
Alternative: Use CloudFront’s built-in image optimization (simpler)
CloudFront recently added automatic image optimization as a managed feature:
{
"ResponseHeadersPolicyId": "managed-image-optimization"
}
This automatically:
- Converts images to WebP (if browser supports)
- Resizes based on device
- Compresses aggressively
Impact
Before:
- JPEG: 1.2 MB per image
- 10 images: 12 MB total
After:
- WebP: 350 KB per image (71% smaller)
- Resized to actual display size: 180 KB (85% smaller)
- 10 images: 1.8 MB total
Result: 12 MB → 1.8 MB (85% reduction)
Page Load Impact
- Image load time: 24s → 3.6s (85% faster)
- Perceived performance: Massive improvement
- Bounce rate: -15-20%
Cost
- Lambda@Edge: ~$5-10/month (1M requests)
- Bandwidth savings: -$20-50/month
- Net cost: $0 to -$40/month
Impact
- Image size: -85%
- Page load time: -40-50%
- Time to implement: 1 hour
Quick Win #6: Database Query Optimization (2-4 Hours, 50-90% Faster Queries)
The Problem
What I typically see:
- Slow database queries (200-500ms)
- Missing indexes on frequently queried columns
- N+1 query problems (1 query for list, then 1 query per item)
Example:
-- Get user's orders (1 query)
SELECT * FROM orders WHERE user_id = 123;
-- For each order, get order items (N queries)
SELECT * FROM order_items WHERE order_id = 456;
SELECT * FROM order_items WHERE order_id = 457;
SELECT * FROM order_items WHERE order_id = 458;
-- ... 100 more queries
The Solution
Add database indexes and optimize queries.
Implementation
Step 1: Identify slow queries
Enable slow query logging on RDS:
aws rds modify-db-parameter-group \
--db-parameter-group-name production-postgres \
--parameters "ParameterName=log_min_duration_statement,ParameterValue=100,ApplyMethod=immediate"
This logs queries taking >100ms.
Step 2: Check query execution plans
-- Analyze slow query
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending'
ORDER BY created_at DESC;
-- Look for "Seq Scan" (full table scan - bad)
-- Want to see "Index Scan" (using index - good)
Step 3: Add missing indexes
-- Add index on frequently queried columns
CREATE INDEX idx_orders_user_id_status ON orders(user_id, status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
Impact:
- Query time: 350ms → 8ms (98% faster)
Step 4: Fix N+1 queries
// Before (N+1 problem)
const orders = await db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
for (const order of orders.rows) {
order.items = await db.query('SELECT * FROM order_items WHERE order_id = $1', [order.id]);
}
// Total queries: 1 + 100 = 101 queries
// After (join or eager loading)
const result = await db.query(`
SELECT
o.*,
json_agg(oi.*) AS items
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
WHERE o.user_id = $1
GROUP BY o.id
`, [userId]);
// Total queries: 1 query
Impact:
- 101 queries → 1 query
- Query time: 3.5s → 45ms (99% faster)
Common Index Patterns
-- Single column (for WHERE clauses)
CREATE INDEX idx_users_email ON users(email);
-- Composite index (for multiple WHERE clauses)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Partial index (for specific subsets)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
-- Index for sorting
CREATE INDEX idx_orders_created_desc ON orders(created_at DESC);
Monitoring Indexes
-- Find missing indexes (PostgreSQL)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY schemaname, tablename;
-- Find unused indexes (safe to drop)
SELECT * FROM pg_stat_user_indexes WHERE idx_scan < 50;
Impact
- Query latency: 350ms → 8ms (98% faster)
- Database CPU: -40-60%
- API response time: -50-70%
- Time to implement: 2-4 hours
Quick Win #7: Enable Gzip Compression on ALB (5 Minutes, 60-70% Bandwidth Reduction)
The Problem
What I typically see:
- Application Load Balancer (ALB) serving uncompressed responses
- API responses: 120 KB JSON uncompressed
- CloudFront compression enabled but ALB responses bypass it
The Solution
Enable gzip compression on ALB target group.
Implementation
Unfortunately, ALB doesn’t support automatic compression. But you can enable it in your application.
Node.js Express:
const compression = require('compression');
app.use(compression({
level: 6, // Compression level (1-9, 6 is default)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Compress all responses unless explicitly disabled
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
Python Flask:
from flask import Flask
from flask_compress import Compress
app = Flask(__name__)
Compress(app)
Impact:
- API response size: 120 KB → 32 KB (73% reduction)
- API latency: -200-300ms (less data to download)
Cost
- $0 (compression is free)
- Bandwidth savings: -$10-30/month
Impact
- Response size: -70%
- API latency: -30%
- Time to implement: 5 minutes
Implementation Checklist
Day 1 (Quick Wins):
- Enable CloudFront compression (5 min)
- Enable HTTP/2 on CloudFront (2 min)
- Enable gzip in application (5 min)
- Total: 12 minutes, ~50% latency improvement
Day 2 (Caching):
- Add CloudFront in front of S3 static assets (1 hour)
- Set up ElastiCache Redis (2-3 hours)
- Total: 3-4 hours, additional 30-40% improvement
Day 3 (Database & Images):
- Optimize database queries and add indexes (2-4 hours)
- Implement image optimization (1 hour)
- Total: 3-5 hours, additional 20-30% improvement
Cumulative impact: 70-85% latency reduction in 3 days
Monitoring Your Improvements
Before you start, measure baseline:
# CloudWatch metrics to track
- ALB TargetResponseTime (p50, p95, p99)
- RDS database CPU and IOPS
- CloudFront cache hit ratio
- Custom application metrics (page load time)
After each change, measure impact:
// Add custom CloudWatch metrics
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();
function recordPageLoadTime(timeMs) {
cloudwatch.putMetricData({
Namespace: 'MyApp/Performance',
MetricData: [
{
MetricName: 'PageLoadTime',
Value: timeMs,
Unit: 'Milliseconds'
}
]
});
}
Create CloudWatch dashboard:
{
"widgets": [
{
"type": "metric",
"properties": {
"metrics": [
["AWS/ApplicationELB", "TargetResponseTime", { "stat": "p95" }],
["MyApp/Performance", "PageLoadTime", { "stat": "p95" }],
["AWS/CloudFront", "CacheHitRate"]
],
"period": 300,
"stat": "Average",
"region": "ap-southeast-2",
"title": "Performance Metrics"
}
}
]
}
Cost vs. Performance Summary
| Optimization | Time | Cost/Month | Performance Gain |
|---|---|---|---|
| CloudFront compression | 5 min | $0 | 60-70% faster |
| CloudFront for S3 | 1 hr | +$10-20 | 50-70% faster |
| ElastiCache | 2-3 hr | +$12 | 80-95% faster (cached) |
| HTTP/2 | 2 min | $0 | 30% faster |
| Image optimization | 1 hr | $0 to -$40 | 40-50% faster |
| Database indexes | 2-4 hr | $0 | 50-90% faster queries |
| Gzip compression | 5 min | $0 | 30% faster |
Total implementation time: 1-2 days
Total monthly cost: +$20 to -$10 (can be net savings from bandwidth reduction)
Total performance improvement: 70-85% latency reduction
Conclusion
These 7 performance optimizations are the highest-impact, lowest-effort improvements I’ve found deliver consistent results:
- CloudFront compression - 5 minutes, 60-70% faster
- CloudFront for S3 static assets - 1 hour, 50-70% faster
- ElastiCache for query caching - 2-3 hours, 80-95% faster cached queries
- HTTP/2 on CloudFront - 2 minutes, 30% faster page loads
- Image optimization - 1 hour, 40-50% faster image loads
- Database query optimization - 2-4 hours, 50-90% faster queries
- Gzip compression in application - 5 minutes, 30% faster API responses
Total time: 1-2 days
Total cost: $20/month (or even net savings)
Total improvement: 70-85% latency reduction
Business impact:
- Conversion rate: +10-15%
- Bounce rate: -15-20%
- Customer satisfaction: Measurably higher
The key is implementing all 7 together—each addresses a different performance bottleneck. Start with the 5-minute wins (compression, HTTP/2), then tackle the bigger optimizations.
Need help optimising your AWS application performance? Our Well-Architected Reviews that include comprehensive performance assessment, prioritised optimisation plan, and hands-on implementation support delivered in weeks not months.