Skip to content

AWS performance optimization: locating the real bottleneck across compute, database, and network

AWS performance optimization starts with finding the one constraint that is actually slow — burst credits, EBS limits, a bad query, or cross-AZ hops — not resizing everything. Here is the diagnostic order.

Most “AWS is slow” complaints trace back to one bottleneck, not general slowness — a burstable instance out of CPU credits, an EBS volume at its IOPS ceiling, one unindexed query, or a chatty service making fifty cross-AZ round trips per request. AWS performance optimization is therefore a diagnosis problem before it is a tuning problem: measure the p95 at the edge, split the time between load balancer, application, and database, and fix the single largest constraint before touching anything else. Resizing instances without that measurement is the most expensive way to change nothing.

What’s happening

A request crosses distinct layers — DNS and CDN, load balancer, application compute, data tier, and network paths between them — and each layer has its own failure signature and its own metrics. The reason unmeasured optimization fails is that these signatures are invisible from the application’s point of view. Three AWS-specific mechanics cause a large share of real cases:

Burstable instances run out of credits. T-family instances (t3, t3a, t4g) earn CPU credits at a baseline rate and spend them to burst. When CPUCreditBalance hits zero on standard mode, the instance is throttled to its baseline — 20-40% CPU for common sizes — and everything on it turns to molasses while the CPU graph deceptively shows “high utilization.” Unlimited mode trades the cliff for surcharges instead.

Storage has separate ceilings from compute. gp3 volumes carry a provisioned IOPS and throughput figure independent of size; gp2 scales IOPS with size and gives smaller volumes a burst bucket whose BurstBalance can silently drain. An instance also has its own EBS bandwidth limit by instance size, so a fast volume behind a small instance is still slow. Details are in the EC2 documentation.

The network bills and delays by geometry. Every cross-AZ hop adds latency and data transfer cost; a request that fans out to services in other AZs multiplies both. NAT Gateway adds another hop for anything leaving private subnets.

The database sits on top of all three and adds its own classics: missing indexes, connection storms, and lock contention — visible in RDS Performance Insights, not in CPU graphs.

What to check first

  • ALB TargetResponseTime p95 versus what the application logs internally — the gap between them is queueing and network, not code.
  • CPUCreditBalance on every t-family instance in the request path. Zero is the smoking gun.
  • BurstBalance on gp2 volumes and IOPS/throughput utilization on gp3.
  • RDS Performance Insights top SQL — one query is usually eating half the load.
  • Whether app and database live in the same AZ pairings or the traffic zig-zags across zones.

How to verify

Start at the edge and work inward:

# p95 latency at the load balancer, last 3 hours
aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB --metric-name TargetResponseTime \
  --dimensions Name=LoadBalancer,Value=app/prod-alb/50dc6c495c0c9188 \
  --start-time 2026-07-04T09:00:00Z --end-time 2026-07-04T12:00:00Z \
  --period 300 --extended-statistics p95

# Burst credit exhaustion on a suspect instance
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 --metric-name CPUCreditBalance \
  --dimensions Name=InstanceId,Value=i-0abc123def456 \
  --start-time 2026-07-04T09:00:00Z --end-time 2026-07-04T12:00:00Z \
  --period 300 --statistics Minimum

# gp2 volume burst bucket
aws cloudwatch get-metric-statistics \
  --namespace AWS/EBS --metric-name BurstBalance \
  --dimensions Name=VolumeId,Value=vol-0abc123def456 \
  --start-time 2026-07-04T09:00:00Z --end-time 2026-07-04T12:00:00Z \
  --period 300 --statistics Minimum

In the console: EC2 > Instances > Monitoring for credit balances, RDS > Databases > Monitoring and Performance Insights for the data tier, and the load balancer’s Monitoring tab for the latency and 5xx picture at the edge.

The runbook

  1. Fix the baseline number first. One p95 latency figure for the top endpoint, measured at the ALB over a representative window. Every later step is judged against it; without it you are guessing with a credit card.
  2. Split the time. ALB TargetResponseTime covers app plus database; application logs or APM split those two. Whichever segment holds the majority of the p95 is the only segment worth working on this week.
  3. Clear the compute throttles. Exhausted credit balances mean moving to unlimited mode (and watching the surcharge) or to m/c-family instances. Check memory pressure too — swapping looks like mysterious latency, and CloudWatch does not show memory without the agent installed.
  4. Clear the storage ceilings. gp2 volumes with sagging BurstBalance migrate to gp3 with explicit IOPS — often cheaper and strictly faster. Confirm the instance’s own EBS bandwidth limit is not the real ceiling before paying for volume IOPS.
  5. Work the database top-down. Performance Insights, top SQL by load, then the standard trio: index the worst query, pool the connections (RDS Proxy if the app churns them), and only then consider instance class. Parameter-level work is covered in RDS parameter group tuning.
  6. Straighten the network path. Keep each request’s fan-out inside one AZ where the architecture allows, put S3 and DynamoDB behind VPC gateway endpoints instead of the NAT, and cache the reads that Performance Insights shows are repeated.
  7. Re-measure and rightsize. Only after the bottleneck moves do sizing decisions mean anything. Compute Optimizer’s rightsizing recommendations are the disciplined way to close the loop — including sizing down what step 3 proved oversized.

When this is the wrong move

If the p95 is fine and one customer on one continent is slow, this is a CDN and geography conversation, not an infrastructure hunt. If latency spikes only at deploy time, look at deployment strategy and connection draining, not steady-state capacity. And if the application makes 200 database queries per page, no amount of AWS tuning outruns that — the fix is in the code, and pretending otherwise just buys a bigger instance to run the same waste faster.

Common mistakes

  • Resizing the instance first, because it is the easiest button — masking the real constraint for a month at double the cost.
  • Trusting CPU graphs on burstable instances: 100% CPU at baseline throttle and 100% CPU under real load look identical unless you check credits.
  • Ignoring the instance-level EBS bandwidth cap and paying for provisioned IOPS the instance physically cannot consume.
  • Benchmarking against staging, which has different instance types, empty caches, and a tenth of the data.

Prevention

  • Alarm on the leading indicators: CPUCreditBalance minimum, BurstBalance, RDS DatabaseConnections, and p95 latency — not just averages.
  • Record the p95 baseline per endpoint after every significant release, so regressions are attributed to a change instead of discovered as folklore.
  • Load-test before traffic events with production-shaped data volumes.
  • Review rightsizing quarterly in both directions; oversized fleets hide bottlenecks and burn budget with equal enthusiasm.

The pattern across all of this: measure at the edge, split the time, fix the one constraint, re-measure. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.