Skip to main content

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

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 TypeBeforeAfterReduction
JavaScript1.2 MB350 KB71%
CSS340 KB95 KB72%
HTML120 KB30 KB75%
JSON API85 KB22 KB74%

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.js instead of app.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

OptimizationTimeCost/MonthPerformance Gain
CloudFront compression5 min$060-70% faster
CloudFront for S31 hr+$10-2050-70% faster
ElastiCache2-3 hr+$1280-95% faster (cached)
HTTP/22 min$030% faster
Image optimization1 hr$0 to -$4040-50% faster
Database indexes2-4 hr$050-90% faster queries
Gzip compression5 min$030% 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:

  1. CloudFront compression - 5 minutes, 60-70% faster
  2. CloudFront for S3 static assets - 1 hour, 50-70% faster
  3. ElastiCache for query caching - 2-3 hours, 80-95% faster cached queries
  4. HTTP/2 on CloudFront - 2 minutes, 30% faster page loads
  5. Image optimization - 1 hour, 40-50% faster image loads
  6. Database query optimization - 2-4 hours, 50-90% faster queries
  7. 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.