Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 072 / 100

Cost optimization on AWS: a hands-on guide for engineers

The strategies that cut real bills.

Expona Admin8 min read

Most teams we've worked with discover they're spending 30–50% more on AWS than they need to. The gap isn't usually about picking the wrong service; it's about not matching what you pay for to what you actually use. Cost optimization on AWS: a hands-on guide for engineers focuses on the strategies that cut real bills without requiring a wholesale migration or sacrificing reliability.

This is day 72 of the 100-day AWS for Developers series. By now you've built systems, deployed to production, and watched metrics flow in. Today we're looking at the financial side—not from an accounting perspective, but from an engineer's practical angle. We'll walk through the four highest-leverage moves: right-sizing instances, committing to compute plans, using spot capacity, and scheduling non-production environments to turn off.

Right-size—Most instances are oversized. Check utilization.

We've inherited dozens of systems where an EC2 instance was provisioned for a projected peak load that never materialized, then never touched again. The instance sits there consuming money every hour, typically running at 10–20% CPU and 30–40% memory.

Right-sizing means matching your instance type to what you actually consume. The data is usually already there—CloudWatch keeps it by default. The friction is that it requires discipline: you have to look at it, make a decision, test a smaller size, and deploy.

Start here: pull CPU and memory metrics for your production instances over the past two weeks.

bash
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2025-01-01T00:00:00Z \
  --end-time 2025-01-15T00:00:00Z \
  --period 3600 \
  --statistics Average,Maximum

If your average CPU is under 20% and your memory stays below 50%, you're almost certainly over-provisioned. A t3.large might run at 5% CPU; a t3.medium might run at 12% CPU and still handle your traffic fine, especially with auto-scaling groups configured with a 60–70% target.

Run this systematically across your fleet. Pick three instances, downsize them by one size class, monitor for 48 hours, then scale the change across your environment. Most teams find they can drop 20–30% of their instance count this way. At scale, that's significant. A team running 50 m5.xlarge instances (which is common for mid-sized backends) might move to 40 m5.large instances and save $300–400 per month with zero functional loss.

Watch out for bursty workloads. If your average CPU is 15% but peak CPU (99th percentile) is 85%, downsize conservatively and pair it with a burstable instance type like t3 or t4g, which accumulate credits during idle periods and spend them during spikes.

Commit for discounts—Savings Plans for steady compute.

Once you've right-sized, lock in your pricing with Savings Plans. If you know you're running a certain baseline of compute every single month, an on-demand EC2 instance costs roughly 100% of the listed price. A one-year Savings Plan on that same compute costs around 30% less. A three-year plan costs 40% less.

Savings Plans work across instance families and regions (with flexibility), so they're not as brittle as the older Reserved Instances. If you buy a one-year plan for m5.large on-demand compute in us-east-1, you can run that workload on m5.xlarge, m6i.large, or t3.large and still get the discount.

The decision framework is straightforward:

  • Predictable, steady load: Buy a Savings Plan. It's the best discount you'll get without operational overhead.
  • Variable or growing workload: Buy a shorter plan (one year instead of three) or skip it in favor of Spot for the variable portion.
  • Experimental or project-based: Stay on-demand. The 20–30% savings isn't worth the commitment risk.

Here's how to evaluate the numbers for your infrastructure. Let's say you run a steady baseline of 20 m5.large instances in production plus another 10 for staging and load testing:

Scenario On-Demand (monthly) 1-Year Plan 3-Year Plan
20 m5.large production (730 hours/month) $1,752 $1,226 $1,051
10 m5.large dev/staging (730 hours/month) $876 $613 $526
Total $2,628 $1,839 $1,577

With a one-year plan, you save $789 per month, or $9,468 per year. Even if you migrate half that workload elsewhere in 18 months, you still broke even on the plan and saved money during the first year.

One caveat: buy plans only after you've stabilized your architecture. Right-sizing first, then committing, ensures you're not paying for capacity you don't need.

Use Spot—Interruptible capacity for workers and batch jobs.

Spot instances are EC2 capacity AWS isn't selling; you get it at 70–90% discount to on-demand. The tradeoff is AWS can reclaim it with two minutes' notice. For production web services that need to be up constantly, Spot is risky. For background workers, batch jobs, data processing, and development environments, it's nearly free compute.

Think through your workload's interruption tolerance:

  • Batch jobs (ETL, image processing, reports): Perfect for Spot. Jobs fail gracefully and retry automatically.
  • Worker queues (email, notifications, analytics): Excellent for Spot. Use SQS with visibility timeout and let jobs re-queue on interruption.
  • Stateless API services: Good for Spot with auto-scaling. A fleet of 10 Spot instances tolerates 1–2 interruptions per day with no user impact.
  • Stateful services (databases, caches): Avoid Spot unless you're running read replicas or non-critical nodes.

Here's how to set up a Spot request for a worker fleet using EC2 Auto Scaling:

bash
aws autoscaling create-launch-configuration \
  --launch-configuration-name worker-lc \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --spot-price "0.03"

aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name worker-asg \
  --launch-configuration-name worker-lc \
  --min-size 2 \
  --max-size 10 \
  --desired-capacity 5 \
  --vpc-zone-identifier "subnet-12345,subnet-67890"

With a spot price of $0.03/hour on m5.large (normally $0.096/hour on-demand), a fleet of 5 instances costs $3.60/day instead of $11.52/day. Over a month, that's $108 versus $345—a saving of 69%. Even with occasional interruptions and job retries, you'll come out far ahead.

Diversify instance types within your Spot request. AWS is less likely to interrupt all your capacity simultaneously if you're running m5.large, m6i.large, and m5.xlarge together. Use mixed instances policy:

bash
aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name worker-asg-mixed \
  --mixed-instances-policy \
    "InstancesDistribution={OnDemandPercentageAboveBaseCapacity=20,SpotAllocationStrategy=lowest-price},LaunchTemplate={LaunchTemplateName=worker-template,Overrides=[{InstanceType=m5.large},{InstanceType=m6i.large},{InstanceType=t3.large}]}" \
  --min-size 2 --max-size 10 --desired-capacity 5

This setup runs 5 instances with 20% on-demand (1 instance) as a safety anchor and 80% Spot (4 instances) for cost. If Spot is interrupted, the on-demand instance keeps serving while the Spot instances restart elsewhere.

Turn things off—Schedule dev environments to stop at night.

Your development, staging, and testing environments run 24/7 by default. Most of the time—nights, weekends, holidays—nobody is using them. That's pure waste.

Scheduling instances to stop outside business hours is one of the easiest wins. Stop (not terminate) an instance and you keep the EBS volume and elastic IP; you just don't pay for compute. A development database running for 16 hours a day instead of 24 is a 33% cost reduction with zero operational burden.

Use AWS Systems Manager to schedule EC2 instances. First, tag your instances:

bash
aws ec2 create-tags \
  --resources i-0123456789abcdef0 \
  --tags Key=Environment,Value=dev Key=Schedule,Value=business-hours

Then create an automation runbook in Systems Manager that stops instances with that tag at 6 PM and starts them at 7 AM on weekdays:

bash
aws ssm create-document \
  --name "StopDevInstances" \
  --document-type "Automation" \
  --content '{
    "schemaVersion": "0.3",
    "description": "Stop dev instances",
    "mainSteps": [
      {
        "name": "StopInstances",
        "action": "aws:executeScript",
        "inputs": {
          "Runtime": "python3.8",
          "Handler": "handler",
          "Script": "import boto3\ndef handler(events, context):\n  ec2 = boto3.client(\"ec2\")\n  response = ec2.describe_instances(Filters=[{\"Name\": \"tag:Schedule\", \"Values\": [\"business-hours\"]}, {\"Name\": \"instance-state-name\", \"Values\": [\"running\"]}])\n  for reservation in response[\"Reservations\"]:\n    for instance in reservation[\"Instances\"]:\n      ec2.stop_instances(InstanceIds=[instance[\"InstanceId\"]])"
        }
      }
    ]
  }'

Hook this to EventBridge with a cron schedule, and you're done. A t3.large dev instance running 16 hours a day (240 hours/month) instead of 24 hours (730 hours/month) costs $48 instead of $73—a saving of $300 per year per instance. With 10 dev instances, that's $3,000 per year.

Take it further: use the same automation to snapshot your databases at the end of the day and spin down RDS instances. Most dev and staging databases can be stopped, not run continuously. You save 50% of the RDS bill with zero risk, since snapshots are cheap and restore quickly.

Common mistakes

  • Committing before right-sizing. Buy a three-year plan on oversized instances and you're locked into waste. Right-size first, commit second.
  • Ignoring data transfer costs. EC2 savings are great, but if you're transferring 10 TB/month between regions or out to the internet, that bill might exceed your compute costs. Optimize data gravity first.
  • Using Spot for latency-sensitive workloads. Spot interruptions are rare, but they're not predictable. Don't put a customer-facing API on Spot unless you have on-demand fallback.
  • Over-optimizing and under-monitoring. If you save $500/month but your incident response time doubles because the system is now under-provisioned, you lost. Monitor before and after any change.
  • Forgetting about NAT gateway costs. NAT gateways charge per GB processed ($0.045/GB in most regions). If you're shipping terabytes through a NAT gateway daily, that can dwarf your EC2 savings. Use NAT instances (EC2) for high-volume workloads, or route through a corporate proxy.

Key takeaways

  • Pull CloudWatch metrics and right-size your instances before making any other optimization decision. Most fleets waste 20–30% on oversized capacity.
  • After right-sizing, buy Savings Plans for predictable, steady workloads. One-year plans typically save 30%; three-year plans save 40% with minimal risk if you're confident in your architecture.
  • Run batch jobs, workers, and non-critical fleets on Spot instances to cut compute costs by 70–90%. Use mixed instance types and on-demand anchors for resilience.
  • Schedule development and staging environments to stop outside business hours. It's a 33% reduction for minimal operational overhead and zero business risk.
  • Combine all four strategies—right-sizing, Savings Plans, Spot, and scheduling—for a realistic 50–60% reduction in EC2 and RDS bills without sacrificing reliability or performance.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles