Skip to content
PRSINDIA

Cloud & DevOps

We cut a client's AWS bill 41% without touching a feature

From $9,420 to $5,560 a month, in eight moves, over six weeks. No rearchitecture, no Kubernetes, no feature removed.

PRS Admin 7 min read

A client came to us with a $9,420 monthly AWS bill for a platform doing about 40,000 daily active users. Nothing was on fire. It had simply grown for three years, and nobody had ever gone back and looked. Six weeks later the bill was $5,560. We removed no features and rewrote nothing.

The order below is the order of payoff, which is roughly the reverse of the order most people attack a cloud bill in.

Step 0: find out what you are actually paying for

Before touching anything, spend a day in Cost Explorer grouped by usage type, not by service. "EC2-Other" being your third-largest line item is a common and completely opaque situation, and the answer is almost always NAT Gateway or EBS. Turn on the Cost and Usage Report, load it somewhere queryable, and enforce a tagging policy so that in three months you can answer "what does this feature cost".

Also: do not buy Savings Plans first. Every instinct says lock in a discount immediately. If you commit before right-sizing, you have just pre-paid for waste for one to three years. Right-size, then commit.

1. The database was six times too big — $1,380/mo

The primary was a db.r6g.4xlarge (16 vCPU, 128 GB). Its CPU utilisation over 90 days averaged 9% and peaked, once, at 31%. Freeable memory never dropped below 70 GB. It had been sized during a load-testing exercise in 2023 and never revisited.

We moved it to a db.r6g.xlarge, in two steps a week apart, watching read/write latency and the buffer cache hit ratio at each. Both stayed flat. That is the whole story: most over-provisioned databases are over-provisioned by an entire order of magnitude, and nobody checks because checking feels risky and doing nothing feels safe.

Note it was already Graviton (r6g). If yours is on r5 or m5, the switch to Graviton is roughly 20% cheaper for equal or better performance on almost every workload, and it is a version-upgrade-and-failover, not a migration.

2. NAT Gateway data processing — $740/mo

This was the single most satisfying find. The application ran in private subnets, which is correct, and therefore all its outbound traffic went through a NAT Gateway. NAT Gateway charges an hourly rate and about $0.045 per GB processed.

Their app was pulling roughly 6 TB a month from S3 — image processing, reading their own uploads — and every byte was being routed out through NAT and back. They were paying a data-processing fee to talk to a service inside AWS.

The fix is a VPC gateway endpoint for S3 and DynamoDB. It is free. It takes ten minutes. It routes that traffic over the AWS backbone instead of through NAT:

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-south-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
}

We added interface endpoints for ECR and Secrets Manager too — those are not free, but they were still cheaper than the NAT processing they replaced, because every container pull was also going through NAT.

If your bill has a big "EC2-Other" or NAT line, check this first. It is the highest ratio of money saved to effort spent in the entire cloud.

3. CloudWatch Logs — $610/mo

Log ingestion is roughly $0.50 per GB and storage accrues on top, and the default retention on a log group is Never Expire. This client had three years of logs, including a debug-level logger that had been switched on during an incident in 2024 and never switched off. It was writing every SQL query the application issued.

  • Turned the debug logger off (this alone was ~40% of the ingestion volume)
  • Set retention: 7 days on application logs, 30 on access logs, 400 on audit logs (a DPDP-adjacent decision — talk to whoever owns compliance before you shorten anything that records access to personal data)
  • Moved anything needed for long-term analysis to S3 with a lifecycle rule to Glacier

Nobody had looked at a log older than a week in the company's history. They were paying to store them forever.

4. EBS: gp2 to gp3 — $290/mo

gp3 is about 20% cheaper per GB than gp2 and decouples IOPS from volume size. On gp2, the only way to get more IOPS is to buy a bigger disk — so teams over-provision capacity to buy performance and then pay for both. Every gp2 volume in the account should be gp3 unless you have measured a reason otherwise. The conversion is live, with no downtime.

While there, we also deleted 3.4 TB of unattached volumes and 140 snapshots of instances that no longer existed. Nobody knew what they were. Nobody claimed them. We tagged and quarantined them for 30 days, then deleted.

5. S3: lifecycle and the garbage nobody sees — $270/mo

Two findings.

First, 1.9 TB of incomplete multipart uploads. When a big upload fails partway, the parts stay in the bucket, invisible in the console's object list, and you pay for them indefinitely. Every bucket you own should have this rule, and most do not:

{
  "Rules": [{
    "ID": "abort-incomplete-mpu",
    "Status": "Enabled",
    "Filter": {},
    "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
  }]
}

Second, everything was in S3 Standard, including six-year-old invoice PDFs that are read approximately never. Intelligent-Tiering on the archive prefixes, with a transition to Glacier Instant Retrieval after 90 days, did the rest.

6. Non-production environments running 24/7 — $340/mo

Three environments — dev, staging, QA — each with an RDS instance and a couple of EC2 boxes, all running continuously. A dev environment is genuinely used maybe 50 hours a week out of 168. Shutting them down evenings and weekends is a ~70% reduction on those resources for zero loss of function.

A Lambda on an EventBridge schedule that stops tagged instances at 21:00 IST and starts them at 08:30 IST on weekdays. Roughly forty lines of Python. The team can start an environment manually if they need it at midnight; in six months, they have done so four times.

7. Load balancers and cross-AZ chatter — $180/mo

Four ALBs, two of which fronted services that had been decommissioned. An idle ALB still costs you its hourly rate plus its LCU floor. Consolidated to two, with host-based routing.

Also worth a look: cross-AZ data transfer. Chatty services spread across availability zones for HA pay $0.01/GB in each direction. It is small until it is not. AZ-aware routing on the internal service mesh trimmed a surprising amount.

8. Only now: Savings Plans — $1,050/mo

With the fleet right-sized and stable, we bought a one-year no-upfront Compute Savings Plan covering the steady-state baseline — deliberately not the peak, because a Savings Plan is a commitment to spend, and over-committing turns a discount into a liability. Cover roughly 70% of your floor; let the burst run on-demand.

Had we done this in week one, at the original instance sizes, we would have locked in a three-year commitment to an oversized database.

The tally

  • RDS right-size + Graviton — $1,380
  • Savings Plan — $1,050
  • NAT Gateway → VPC endpoints — $740
  • CloudWatch retention + log hygiene — $610
  • Non-prod scheduling — $340
  • gp2 → gp3 + orphan cleanup — $290
  • S3 lifecycle + multipart cleanup — $270
  • ALB consolidation + cross-AZ — $180
  • Total: $3,860/mo — 41%

What we deliberately did not do

  • Move to Kubernetes. Suggested by someone at every one of these engagements. It is a bet that platform complexity will be repaid in bin-packing efficiency, and for a team of six it will not be. They stayed on ECS Fargate.
  • Spot instances for stateful workloads. Great for batch, terrible for anything that holds a session.
  • Aggressive reserved-instance commitments. The company was growing and its shape was changing. One year, no upfront, partial coverage.

Keeping it down

A cost review is worthless if the bill just regrows. Three things, all boring:

  1. AWS Budgets with anomaly detection, alerting to the engineering channel — not to finance, who cannot act on it.
  2. A mandatory tagging policy enforced by an SCP. Untagged resources get flagged weekly.
  3. Thirty minutes on the monthly engineering agenda, looking at the top five movers. That is it.
Cloud bills do not grow because engineers are careless. They grow because nobody owns the number, and every individual decision that added $40 a month was locally reasonable.

Written by

PRS Admin

Building software at PRS India.

Keep reading

All articles

Engineering

Server Components changed how we structure Next.js apps — our current default

Read it

Engineering

Laravel queues in production: four failure modes nobody warns you about

Read it

Business of Software

What an ERP migration actually costs — a line-item breakdown

Read it