All posts
Database CostJun 23, 20267 min read

Why Your Aurora Bill Doubled (And It's Not Your Traffic)

Your Aurora bill is exploding from I/O you don't even see. It's not traffic, it's inefficient query plans, oversized instances, and dev-cluster sprawl draining your budget. Here's the engineering deep dive.

VS

Venkat Sakamuri

DeepSQL R&D · Ex Oracle Query Engine Team · YC & CMU

An illustration showing a direct path versus a complex, inefficient path to the same destination.

TL;DR

  • Your bill isn't high because of traffic, it's high because of I/O. When your I/O cost exceeds 25% of your compute, AWS automatically moves you to the I/O-Optimized tier, which can easily double your bill. This is a tax on bad queries.
  • Sequential scans are the enemy. A single unindexed query scanning a large table forces millions of 8KB page fetches from the storage layer, each one adding to your I/O bill. This churn also evicts useful data from the buffer cache, creating a vicious cycle.
  • You're burning cash on ghost infrastructure. That idle read replica from a cancelled BI project and the r6g.4xlarge staging cluster are costing you thousands a month for zero business value. Autoscaling won't save you; it scales up for bad queries and rarely scales down.

Six months ago, your monthly Aurora Postgres bill was $15,000. Now it's $31,000. Your traffic is flat. The finance team is asking pointed questions. Your first instinct is to blame AWS pricing, file a support ticket, and hope for a magical credit. The truth is much simpler and harder to swallow: the problem is almost certainly inside your database, caused by queries and configurations your team put there.

I spent years on the Oracle query engine team before starting DeepSQL. I've seen this exact story play out a hundred times. A seemingly minor code change introduces a bad query plan, or infrastructure gets provisioned and forgotten. The costs don't spike overnight; they creep up insidiously. By the time the bill is alarming, nobody remembers the root cause.

Let's break down the real reasons your bill is out of control. It's not black magic. It's math, query plans, and operational neglect.

The I/O-Optimized Trap

Amazon's pricing for Aurora is a masterclass in monetizing inefficiency. You start on the standard tier, paying for your instance hours, storage, and a per-I/O rate. But there's a catch. If your I/O operations cost more than 25% of your total Aurora database compute costs in a given month, AWS will automatically—and mandatorily—move your cluster to the "I/O-Optimized" configuration.

On this new tier, you pay a ~30% premium on your instance hours and storage, but you get "free" I/O. This sounds like a good deal, but it's not. It's a punishment. It means your workload is so inefficient that Amazon can make more money by charging you a flat premium. You've gone from paying for what you use to paying a tax on your database's poor performance.

What is an "I/O operation" in this context? It's a read request from the Aurora storage layer. For Postgres, this is an 8KB page; for MySQL, it's a 16KB page. When your query needs data that isn't in the instance's buffer cache (RAM), it has to fetch it from storage. Each fetch is a billable I/O.

This is where query plans become critical. Consider this common query:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE created_at > (NOW() - INTERVAL '1 day') AND status = 'pending';

If you don't have an appropriate index (e.g., on (status, created_at)), Postgres has no choice but to perform a Sequential Scan.

                                                          QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on orders  (cost=0.00..564013.25 rows=100023 width=212) (actual time=12.3..4567.8 rows=98765 loops=1)
   Filter: ((status = 'pending'::text) AND (created_at > (now() - '1 day'::interval)))
   Rows Removed by Filter: 9901235
   Buffers: shared read=403251
 Planning Time: 0.215 ms
 Execution Time: 4598.123 ms
(6 rows)

Look at that plan. The Seq Scan tells you it's reading the entire table. Rows Removed by Filter: 9901235 shows it read almost 10 million rows from disk only to throw them away. But the most expensive line item is Buffers: shared read=403251.

This means to satisfy your query, Postgres had to read 403,251 8KB pages from the Aurora storage layer. That's 403,251 billable I/O operations for a single query. Now imagine this query running a few times a minute. It adds up. We're talking millions of I/Os per hour, easily enough to trip the 25% I/O threshold and get your cluster "upgraded" to the I/O-Optimized tier.

A correct index would change this to an Index Scan with maybe a few hundred shared reads, keeping you far below the I/O cost threshold.

This isn't just about one bad query. A few of these running concurrently create "buffer cache churn." One sequential scan kicks another's pages out of RAM, forcing both to go back to storage on their next execution. Your buffer hit ratio plummets, and your I/O bill skyrockets.

The Silent Killers: Instance Sprawl and Oversizing

The second major cost driver is far less technical but just as venomous: paying for compute you don't use.

Oversized Instances: The panic reaction to a slow query is often "scale up the instance." Your db.r6g.2xlarge is slow, so you bump it to a 4xlarge. The extra CPU and, more importantly, larger buffer cache, might make the pain go away temporarily. But the underlying Seq Scan is still there, now just running faster. You've masked the problem, not solved it. The 4xlarge becomes the new baseline, and nobody dares scale it down for fear of breaking something. You're now paying double the hourly instance cost, forever.

Dev/Staging Waste: This is the most egregious waste I see. An engineer spins up a staging environment, clones the production database, and attaches it to a beefy db.r6g.4xlarge to run some tests. The tests finish, the engineer moves on, and the cluster is forgotten. It sits there, running 24/7, serving a handful of queries per day, burning thousands of dollars a month. This happens in every single company of a certain size. Go check your AWS account right now; I guarantee you'll find at least one.

Idle Replicas: The same logic applies to read replicas. Someone in marketing wants to hook up a BI tool. They request a read replica. Your team provisions one. The BI project loses funding or they switch tools, but the replica remains. It's still replicating all write traffic (generating WAL traffic and I/O), its instance is running 24/7, and it's serving zero queries. It's a ghost, and it's costing you 100% of its instance price.

Aurora Serverless v2 is marketed as the solution, but be wary. It scales up Aggressively when it sees CPU or memory pressure—like from a bad Seq Scan—but it scales down very, very slowly. This asymmetrical behavior means a short-term performance issue can result in a permanently higher cost baseline as your cluster gets stuck at a high number of ACUs (Aurora Capacity Units).

What DeepSQL does about this

We built DeepSQL to fix this systematically, not with one-off consulting gigs. Our agent attaches to your database and observes the live workload by parsing sources like pg_stat_statements and the MySQL Performance Schema. It attributes cost—CPU, memory, and estimated I/O—down to the individual query pattern, turning your opaque bill into an itemized list of offenders. Instead of just showing you a dashboard of slow queries, our engine generates the exact CREATE INDEX statement or query rewrite required to fix the underlying plan. For a recent e-commerce client, this process cut their Aurora bill by 38% in the first month by fixing 12 queries and providing the data to safely downsize two forgotten staging clusters.

An illustration of three database instances, two of which are idle and one of which is vastly underutilized.