All blogs
    AWSCost OptimizationStartup

    Our AWS Bill Was $900+. Here’s Where the Money Went.

    August 13, 202610 min read
    Our AWS Bill Was $900+. Here’s Where the Money Went.

    There is a very specific feeling that only startup founders and infra engineers know.

    It's the 3rd of the month. You open the AWS billing console just to "check something." You expect the usual number. Instead you see a number that is almost double the usual number. Your stomach does a small backflip. You refresh the page, because obviously AWS made a mistake. AWS did not make a mistake.

    That was us. Our bill went from a comfortable $500–600 to over $1,000, and nobody had shipped anything that dramatic. No viral launch. No 10x traffic. No new product. Just... a bigger bill.

    This post is the story of where that money went, and the checklist we now run so it doesn't happen again.

    Why this matters more than founders think#

    Startups optimize everything. You negotiate SaaS contracts. You argue about whether you really need that $20/month tool. Someone in the team definitely brings homemade lunch to save money.

    But infrastructure? Infrastructure is a black box that silently withdraws money from your bank account every month, and most teams have no idea what's inside it.

    Here's the thing: your AWS bill is not a bill. It's a report card. It tells you exactly how disciplined your engineering practices are. Untagged resources, forgotten staging environments, oversized instances, caches nobody tuned - all of it shows up as a line item eventually.

    Cost optimization isn't a finance skill. It's an engineering skill. And for an early-stage startup, $400/month of waste is a real chunk of runway.

    Where our money actually went#

    We sat down and did the forensics. Here's what we found, roughly in order of how much it hurt.

    1. We were paying full price for machines we knew we'd keep#

    Our baseline workload - the EC2 instances and RDS databases that are always running - was sitting on plain on-demand pricing. We knew those machines weren't going anywhere. We'd been running them for over a year.

    Paying on-demand for a server you're certain you'll keep for 12 months is like paying daily hotel rates for an apartment you already live in.

    Moving that steady baseline to Reserved Instances / Savings Plans was the single biggest win. Not everything - just the boring, predictable, always-on stuff.

    2. Zombie staging environments#

    We spin up staging environments constantly. Test a migration, demo a feature, reproduce a bug. That part is healthy.

    What's not healthy is that we spin them up and then... just leave. The feature ships, everyone moves on, and somewhere in us-east-1 a t3.medium and an RDS instance are still running at 3 AM, serving absolutely no one, faithfully billing us every hour.

    3. The RDS resurrection#

    This one deserves its own section because it caught us properly.

    You can stop an RDS instance. It feels like you've saved money. You have - for exactly seven days. After seven days, AWS automatically starts it back up. It's documented behavior, not a bug. Stopping is meant for short pauses, not for parking something indefinitely.

    So our "stopped" staging databases had quietly resurrected themselves and were billing us for weeks. Nobody noticed, because nobody was looking at them - they were stopped, right?

    If you're done with a database, snapshot it and delete it. Snapshot storage costs pennies. A running db.t3.medium does not.

    4. Nothing was tagged#

    This is the root cause behind half of the above. When you open Cost Explorer and see a $180 line item, the only useful question is: whose is this, and can we kill it?

    Without tags, you can't answer that. So the resource survives, because nobody wants to be the person who deleted production.

    Untagged infrastructure is infrastructure nobody owns. And infrastructure nobody owns never gets turned off.

    5. Storage we forgot existed#

    Terminate an EC2 instance and its EBS volume can stick around. Take snapshots "just in case" and they accumulate forever. Ship logs to CloudWatch with default retention and they live until the heat death of the universe.

    None of these are expensive individually. All of them together, over a year, are absolutely a line item.

    6. Caching that only ever grew#

    We use Redis for caching, which is great. But we had never really answered two questions: what deserves to be cached, and when does it leave.

    Data volume grows. Keys accumulate. Eventually you're paying for a bigger cache node to store things nobody has read in months. A cache without eviction policy and TTLs isn't a cache - it's a database you didn't plan for.

    What actually changed#

    Bringing the bill back down wasn't one heroic fix. It was a boring list of small ones:

    • Reserved capacity for the predictable baseline (EC2 + RDS)
    • Deleted the zombie staging environments, and snapshot-and-delete instead of stop for non-prod databases
    • A real tagging policy, enforced
    • Cleaned up unattached volumes, ancient snapshots, and unmounted disks
    • TTLs and an eviction policy on Redis, plus right-sizing the node
    • Budget alerts, so next time we find out on day 3 instead of day 30

    The bill came back down. More importantly, it now stays down, because someone is actually watching it.

    The checklist#

    Here's the full list, organized so you can work through it category by category. You will not need all of it. Do the compute and governance sections first - that's where most of the money is.

    Compute (EC2, containers, Lambda)#

    • Right-size before you commit. Check AWS Compute Optimizer. Most instances are running at 10–20% CPU. Downsize first, then buy a Savings Plan - otherwise you've locked in the wrong size for a year.
    • Buy Savings Plans or Reserved Instances for your steady baseline. Start with 1-year, no-upfront. Lower discount, but far less risky for a startup that might pivot.
    • Never commit for spiky or experimental workloads. Commitments are for the boring always-on stuff only.
    • Auto-shutdown dev and staging outside work hours. A machine running 12 hours a day on weekdays costs ~35% of a 24/7 machine. EventBridge + Lambda, or AWS Instance Scheduler.
    • Use Spot instances for CI runners, batch jobs, and workers that can tolerate interruption. Up to ~70–90% cheaper.
    • Try Graviton (ARM) instances. Roughly 20% better price-performance for most standard workloads. Usually just a base-image change.
    • Delete idle load balancers. Every ALB/NLB charges by the hour whether or not traffic flows through it.
    • Check your Lambda memory settings. Over-provisioned memory on a high-invocation function adds up fast.

    Databases (RDS, Aurora, DynamoDB)#

    • Reserved Instances for your production database. It's the most predictable workload you own.
    • Delete non-prod databases, don't stop them. Stopped RDS instances restart automatically after 7 days. Snapshot and delete instead.
    • Turn off Multi-AZ on everything that isn't production. You're paying double for redundancy on a staging database.
    • Migrate storage from gp2 to gp3. Cheaper per GB and you can tune IOPS independently. Usually a no-downtime change.
    • Check your backup storage. You get free backup storage equal to your provisioned DB storage; beyond that, you pay.
    • Audit retention on automated backups and manual snapshots. Manual snapshots live forever until someone deletes them.
    • Consider Aurora Serverless v2 for spiky or non-prod workloads that sit idle most of the day.
    • Check DynamoDB capacity mode. On-demand is great for unpredictable traffic and expensive for steady traffic. Provisioned + autoscaling is often cheaper once you have a stable pattern.

    Storage (EBS, S3, snapshots, logs)#

    • Find unattached EBS volumes. Terminated instances leave orphans behind. Sort volumes by state and delete anything unattached that you can identify.
    • Delete old snapshots and stale AMIs. Set a retention policy (e.g. keep 30 days) with Data Lifecycle Manager instead of doing it by hand.
    • Add S3 lifecycle policies. Move data to Infrequent Access after 30 days, Glacier after 90, delete when it's genuinely dead.
    • Enable S3 Intelligent-Tiering for buckets with unpredictable access patterns.
    • Clean up incomplete multipart uploads. These are invisible in the console and you are absolutely paying for them. One lifecycle rule fixes it forever.
    • Set CloudWatch log retention. The default is "never expire." Set 30 or 90 days on everything that isn't compliance-relevant.

    Networking (the sneaky one)#

    • Audit your NAT Gateways. In most regions each one costs roughly $32/month before a single byte passes through, plus a per-GB data processing charge. Teams routinely run one per AZ without thinking about it.
    • Add VPC endpoints for S3 and DynamoDB. Gateway endpoints are free and stop that traffic from going through the NAT Gateway.
    • Watch cross-AZ data transfer. Chatty microservices spread across availability zones pay a per-GB tax on every internal call.
    • Serve public traffic through CloudFront. Egress via CloudFront is generally cheaper than straight from EC2 or S3.
    • Release unused Elastic IPs. Public IPv4 addresses are billed hourly now - including ones sitting idle.

    Caching (Redis / ElastiCache)#

    • Cache what's expensive, not what's easy. Slow queries and repeated external API calls, not everything that happens to be a function return.
    • Every key gets a TTL. No exceptions. A key without an expiry is a memory leak with a monthly invoice.
    • Set an eviction policy (allkeys-lru is a sane default for a pure cache).
    • Monitor your hit rate. Below ~80%, you're mostly paying for a node that isn't saving you much.
    • Right-size the node. Check actual memory used vs. provisioned before you scale up.

    Governance (do this first, honestly)#

    • Enforce a tagging policy. At minimum: Environment, Owner, Project. Add ExpiresOn for anything temporary - future you will thank present you.
    • Enforce it with automation, not good intentions. Tag Policies, SCPs, or an IaC pre-commit check. "Everyone please remember to tag" has a 0% success rate.
    • Set up AWS Budgets with alerts at 50%, 80%, and 100% of expected spend. This is 5 minutes of work and it's the reason you'll never be surprised again.
    • Enable Cost Anomaly Detection. It catches the weird spikes automatically.
    • Separate accounts for prod, staging, and dev via AWS Organizations. Costs become instantly attributable and blast radius shrinks.
    • Put a 20-minute cost review on the calendar. Monthly minimum. Open Cost Explorer, group by tag, ask "what is this and do we need it?"
    • Check Trusted Advisor and Compute Optimizer. Free recommendations you're already entitled to.

    The part that actually matters#

    The specific savings aren't the point. The tools change, the pricing changes, AWS launches a new instance family every other week.

    The point is that infrastructure cost is a feedback loop about how you build. A bill that creeps up month over month is telling you that resources are being created faster than they're being owned. That's an organizational problem long before it's a billing problem.

    So: tag everything, delete aggressively, commit only to what's genuinely permanent, and look at the number before it becomes a surprise.

    Your runway is finite. Don't spend it on a staging database that resurrected itself in July and has been running ever since.

    Thanks for reading!
    More bite-sized engineering notes are waiting on the blog.

    © 2026 Arpan Pokharel