# When is RDS PostgreSQL better than Aurora?

> **Quick answer:** RDS is better when your database fits inside the 64 TiB that
> the
> [AWS storage documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html),
> read on 2026-09-19, gives for most DB instance classes, a minute of failover
> is acceptable, and you need something on
> Aurora's incompatibility list. Aurora is worth it for storage growth you
> cannot predict, failover in seconds, and readers that must be close to the
> writer. The deciding factor is rarely raw speed, because the thing that makes
> a query slow is usually the query.

## What is the question?

Aurora is often presented as "RDS but better", and the correction usually
offered is that Aurora is the same PostgreSQL on different storage. Both claims
are misleading in ways that cost money.

Aurora PostgreSQL is PostgreSQL-compatible. It uses the PostgreSQL planner and
executor, so indexing, statistics and query design carry over unchanged. It
also has its own supported-extension matrix, release calendar, parameter set,
metrics and billing model. The storage layer explains much of the difference.
It does not explain all of it, and "it is just the storage" is the assumption
that leads teams to discover an unsupported extension during a migration.

The other half of the framing that needs fixing is the RDS side. "RDS for
PostgreSQL" is not one architecture. It is at least three, and they differ in
the things this comparison covers.

## Five things you are actually choosing between

| deployment | writer | readable replicas | replication |
|---|---|---|---|
| RDS Single-AZ | one instance | external read replicas only | asynchronous physical |
| RDS Multi-AZ DB instance | one instance, one standby | none, the standby does not serve reads | synchronous to the standby |
| RDS Multi-AZ DB cluster | one writer | two readers, across three AZs | semisynchronous, native |
| Aurora provisioned | one writer | up to 15 Aurora Replicas | shared cluster volume |
| Aurora serverless | one writer | up to 15 | shared cluster volume |

The row most often missing from comparisons is the Multi-AZ DB cluster, which is
the one that makes "only Aurora gives you readable high-availability replicas"
false. Read on 2026-09-19, the
[Multi-AZ DB cluster documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html)
says: "A Multi-AZ DB cluster has a writer DB instance and two reader DB instances
in three separate Availability Zones in the same AWS Region."

On the replication model, the same
[Multi-AZ DB cluster documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html),
read on 2026-09-19, says: "Multi-AZ DB cluster deployments use semisynchronous
replication, which requires acknowledgment from at least one reader DB instance
in order for a change to be committed. It doesn't require acknowledgment that
events have been fully executed and committed on *all* replicas."

So a commit waits for one reader to acknowledge receipt. That is not a guarantee
that either reader has applied it, which matters for the consistency question
below.

## What Aurora gives you, from AWS documentation

Every row below was read from AWS documentation on 2026-09-19 and is not measured
here. Each row links the page it came from. These are service limits subject to
engine version and Region, not properties of PostgreSQL.

| | RDS for PostgreSQL | Aurora PostgreSQL |
|---|---|---|
| maximum storage | 64 TiB, most instance classes | 256 TiB on PG 17.5+, 16.9+, 15.13+; 128 TiB below |
| storage growth | you resize, or you enable autoscaling | grows with the data, automatically |
| readable replicas | 2 on a Multi-AZ DB cluster, plus external read replicas | up to 15 Aurora Replicas |
| replica lag | varies; flow control can bound it on a Multi-AZ DB cluster | "usually considerably less than 100 milliseconds" ([performance and scaling](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Performance.html)) |
| failover | depends on deployment type | "typically restored in less than 60 seconds, and often less than 30 seconds" ([high availability](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html)) |
| serverless | no equivalent | Aurora serverless, minimum capacity 0 ACUs on PG 16.3, 15.7, 14.12, 13.15 and higher |

**Maximum storage.** Read on 2026-09-19, the
[Amazon RDS DB instance storage page](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
lists "PostgreSQL 64 TiB" as the maximum most DB instance classes scale to, with
smaller ceilings on burstable classes: 32 TiB on `db.t4g.medium` and
`db.t3.medium`, 16 TiB on `db.t4g.small`, 6 TiB on `db.t4g.micro`. The
[Aurora quotas page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_Limits.html)
gives the Aurora ceiling by version: "256 TiB maximum: Aurora PostgreSQL
versions: 17.5 and all higher versions, 16.9 and higher, 15.13 and higher", and
"128 TiB maximum: All earlier Aurora PostgreSQL versions".

**Storage growth.** Read on 2026-09-19, the
[Aurora performance and scaling page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Performance.html)
says "Aurora storage automatically scales with the data in your cluster volume",
and that on all supported Aurora PostgreSQL versions the volume also shrinks when
you drop data. This is the difference people mean when they say "Aurora is
elastic", and it remains an operational difference below 64 TiB.

**Readable replicas.** Read on 2026-09-19, the
[Aurora high availability page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html)
says "you can create up to 15 read-only Aurora Replicas".

The sentence right after it on that same
[Aurora high availability page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html),
read on 2026-09-19, says: "Aurora Replicas use asynchronous replication to support
high availability without affecting primary instance performance." Asynchronous,
in AWS's own words.

**Replica lag.** Read on 2026-09-19, the
[Aurora performance and scaling page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Performance.html)
says each replica "returns the same data from the cluster volume with minimal
replica lag, usually considerably less than 100 milliseconds after the primary
instance has written an update". "Usually considerably less" is not a bound. For
RDS Multi-AZ DB clusters the Multi-AZ page describes flow control, which is
covered below.

**Failover.** Read on 2026-09-19, the
[Aurora high availability page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html)
says: "A failure event results in a brief interruption, during which read and
write operations fail with an exception. However, service is typically restored
in less than 60 seconds, and often less than 30 seconds." Two things in that
sentence get dropped when it is quoted: read and write operations fail with an
exception, and the number is database promotion, not application recovery.

If the cluster has no Aurora Replica, the
[same page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html),
read on 2026-09-19, says a new primary is created instead, which "typically
takes less than 10 minutes".

**Serverless.** Read on 2026-09-19, the
[auto-pause page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html)
gives the eligibility for scaling to zero: "If you're using Aurora PostgreSQL,
the database engine must be running at least version 16.3, 15.7, 14.12, or
13.15", the instance class must be `db.serverless`, and minimum capacity must be
set to 0 ACUs. The idle interval before pausing is configurable, minimum 300
seconds, default 300 seconds, maximum 86,400. Resuming is not free: "the typical
time to resume might be approximately 15 seconds", and after more than 24 hours
paused, "the resume time can be 30 seconds or longer".

## Aurora does not give you read-after-write consistency

This is the most common way the low-lag number gets misused. An Aurora Replica
has its own compute and its own page cache. It does not see the writer's memory.
Read on 2026-09-19, the
[Aurora PostgreSQL replication page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Replication.html)
is explicit about what the lag metric measures: "The `ReplicaLag` metric for an
Aurora Replica indicates the lag for the page cache of the Aurora Replica
compared to that of the writer DB instance."

AWS publishes a lag metric because readers lag. The routing rule is therefore a
rule, not an inference from a typical number:

- Reads that must see the caller's own write go to the writer endpoint.
- Reads that tolerate bounded staleness go to the reader endpoint.
- If you need a cross-request guarantee, implement it in the application's
  transaction routing. Do not derive it from the
  [performance page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Performance.html)'s
  "usually considerably less than 100 milliseconds", read on 2026-09-19.

The same applies to an RDS Multi-AZ DB cluster. Semisynchronous means one reader
acknowledged the change, not that the reader you are about to query has applied
it.

## Where the difference is not: the query

Both services run the PostgreSQL planner. The most common reason a query is slow
is the query, and no storage layer fixes that.

Measured on 2026-09-19 against PostgreSQL 17.11 in a stock `postgres:17.11`
container on an 8 vCPU Linux 6.17.0-1022-azure host under Docker 29.1.3, default
settings including `shared_buffers` at 128MB. The table is 2,000,000 rows, 158 MB
total, 115 MB heap, of which 2,000 rows match the predicate:

<!-- sql-check: skip needs the two million row bench table built above it -->
```sql
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT count(*) FROM bench WHERE status = 'pending';
```

Without an index, three consecutive runs on 2026-09-19 on that PostgreSQL 17.11
`postgres:17.11` container gave execution times of 115.459 ms, 97.066 ms and
98.775 ms. The plan:

```
 Finalize Aggregate (actual time=90.109..97.019 rows=1 loops=1)
   Buffers: shared hit=12290 read=2416
   ->  Gather (actual time=89.969..97.007 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate (actual time=85.285..85.287 rows=1 loops=3)
               ->  Parallel Seq Scan on bench (actual time=0.116..85.107 rows=667 loops=3)
                     Filter: (status = 'pending'::text)
                     Rows Removed by Filter: 666000
 Execution Time: 97.066 ms
```

The usual shorthand gets this plan wrong: "no
usable index" does not mean a sequential scan. Here it is a **parallel**
sequential scan with two launched workers. A predicate with no usable access path
can also come out as a bitmap heap scan, a partition-pruned scan or a hash join
feeding an aggregate, depending on selectivity, table size, the visibility map
and `work_mem`. The planner picks the cheapest estimated plan from more
options than "the index or a seq scan".

Adding one index, which on 2026-09-19 on that PostgreSQL 17.11 `postgres:17.11`
container took 1.446 seconds and occupies 13 MB:

<!-- sql-check: skip same table -->
```sql
CREATE INDEX bench_status_idx ON bench (status);
```

Three runs then gave 0.316 ms, 0.269 ms and 0.374 ms, same container, same
2026-09-19, same PostgreSQL 17.11:

```
 Aggregate (actual time=0.255..0.255 rows=1 loops=1)
   Buffers: shared hit=6
   ->  Index Only Scan using bench_status_idx on bench (actual time=0.016..0.156 rows=2000 loops=1)
         Index Cond: (status = 'pending'::text)
         Heap Fetches: 0
 Execution Time: 0.269 ms
```

Execution time fell from 97.066 ms to 0.269 ms, and the buffers touched fell
from 12,290 hits and 2,416 reads to 6, on 2026-09-19 on PostgreSQL 17.11 in that
`postgres:17.11` container. Two caveats apply to those numbers:

It demonstrates the value of a fitting index **for that query on that table**. It
is not a ceiling on what infrastructure can be worth. A workload bounded by
memory, by CPU parallelism, by read latency or by recovery time can gain very
large factors from a different instance class or a different storage layer. The
claim the measurement supports is narrower and still useful: before you compare
engines, find out whether your slow query has a plan problem, because that is
free to fix and neither engine fixes it for you.

## Cache hit rate does not tell you whether storage is your problem

This diagnostic is the one most likely to send you to the wrong answer. The
counter-example below was measured on 2026-09-19 on the same PostgreSQL 17.11
`postgres:17.11` container described above.

Three thousand single-row inserts, each its own transaction, run twice against
the same table with nothing else changed but `synchronous_commit`. Measured on
2026-09-19 against PostgreSQL 17.11 in that same stock `postgres:17.11`
container, Linux 6.17.0-1022-azure under Docker 29.1.3:

| `synchronous_commit` | wall clock for 3,000 commits | heap blocks read | heap blocks hit | hit rate |
|---|---|---|---|---|
| `on` | 13.143 s | 0 | 3,030 | 100.00% |
| `off` | 0.284 s | 0 | 3,030 | 100.00% |

Commit throughput differed by a factor of 46, entirely in the storage layer, with
a perfect cache hit rate on both runs. The rate query many articles recommend is:

<!-- sql-check: postgres -->
```sql
SELECT sum(heap_blks_read)  AS heap_read,
       sum(heap_blks_hit)   AS heap_hit,
       round(100.0 * sum(heap_blks_hit)
             / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0), 2) AS hit_pct
FROM pg_statio_user_tables;
```

It reported `100.00` in both rows of that table. It is a table-access aggregate,
cumulative since the last stats reset, and it does not see WAL flushes,
checkpoint writes, temp file spills, index reads counted elsewhere, or anything
outside user table access. A high number means table reads were served from
memory during the window. It does not mean the storage layer is not what is
holding you up.

Use it as context. Diagnose with per-query buffers from
`EXPLAIN (ANALYZE, BUFFERS)`, with wait events, and with read and write latency
and queue depth from CloudWatch or Performance Insights.

The sequential scan counters are still worth checking, for the different question
of whether a plan is the problem:

<!-- sql-check: postgres -->
```sql
SELECT relname,
       seq_scan,
       seq_tup_read,
       idx_scan,
       seq_tup_read / NULLIF(seq_scan, 0) AS rows_per_seq_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 20;
```

## The replica difference, stated accurately

An RDS read replica is a PostgreSQL streaming standby that replays WAL. An Aurora
Replica attaches to the same cluster volume as the writer, with its own compute
and its own page cache. That difference is real, but it does not remove
query cancellation.

### The cancellation happens on both, for different reasons

Measured on 2026-09-19 on a streaming standby of that PostgreSQL 17.11 primary,
another stock `postgres:17.11` container on the same Linux 6.17.0-1022-azure host
under Docker 29.1.3, with `max_standby_streaming_delay` at 5s (the PostgreSQL default is 30s) and
`hot_standby_feedback` off. A `REPEATABLE READ` transaction on the
standby held a snapshot across a `pg_sleep(25)` while the primary deleted and
vacuumed the rows it had read:

```
BEGIN
 count
--------
 200000
ERROR:  canceling statement due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.
ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK
```

`hot_standby_feedback = on` prevents it, at the cost of holding back cleanup on
the primary.

Now the part usually reported as an Aurora advantage. Read on 2026-09-19, the
[Aurora PostgreSQL replication page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Replication.html)
documents the same message: "When a read-only query is canceled due to a recovery
conflict, you might see one or more of the following error messages in the
database error log: `Canceling statement due to conflict with recovery`."

The same
[Aurora PostgreSQL replication page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Replication.html),
read on 2026-09-19, lists the conditions under which an Aurora Replica restarts:
when it cannot stream data from the writer during replication recovery, when a
second recovery starts while one is in progress, "when your DB instance is
nearing the transaction ID wraparound", and "when the replication process is
blocked under certain circumstances".

So it is wrong to say neither behaviour exists on Aurora. What Aurora lacks is
the specific PostgreSQL standby mechanism: a replica replaying WAL that removes
row versions an open snapshot still needs. Your
application still has to retry a cancelled read on either service.

### Lag is shaped by settings, not only by write volume

A bulk insert raises replica lag, which makes "lag follows your write volume" an
easy conclusion and a wrong one. The test below produces lag from a setting, with
essentially no write volume at all.

Measured on 2026-09-19 on that same PostgreSQL 17.11 primary and standby pair,
both stock `postgres:17.11` containers. `recovery_min_apply_delay` was set to
`30s` on the standby, then a **single row** was inserted on an otherwise idle
primary:

| time since insert | row visible on standby | `pg_last_xact_replay_timestamp()` lag |
|---|---|---|
| 3 s | no | 12 s |
| 10 s | no | 19 s |
| 20 s | no | 29 s |
| 33 s | yes | 33 s |
| 40 s | yes | 40 s |

A parameter produced thirty seconds of lag from one row of write volume. Lag is
shaped by WAL generation, network throughput, replay speed, replica sizing,
conflicts, long-running transactions and configuration, and several of those are
settings you choose.

On the managed services the same is true in both directions. Read on 2026-09-19,
the
[Multi-AZ DB cluster documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html)
describes flow control for RDS for PostgreSQL Multi-AZ DB clusters: a background
worker on the writer "adds a delay at the end of a transaction" when lag exceeds
a threshold, and "to control the lag threshold, use the parameter
`flow_control.target_standby_apply_lag`". That is an RDS setting that trades write
throughput for bounded replica lag, and it is missing from most comparisons.

## Compatibility is often the actual deciding question

For a lot of teams this decides it before size or lag are discussed, and it is
the section most comparisons leave out. Aurora PostgreSQL publishes its own
[extension support matrix](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/AuroraPostgreSQL.Extensions.html)
and its own
[release calendar](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/aurorapostgresql-release-calendar.html),
separate from the
[RDS for PostgreSQL release calendar](https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-release-calendar.html).

Make the checklist before comparing anything else:

- The exact PostgreSQL major and minor version you need, and the upgrade path.
- Every extension you use, with its version, checked against Aurora's matrix.
- Parameter group settings you rely on, and which of them are static.
- Logical replication and change-data-capture tooling, including slots.
- Anything that depends on superuser, the file system, or a background worker.
- Backup, point-in-time recovery, cross-Region disaster recovery, migration.
- The observability integrations your existing runbooks already reference.

One caveat on the way this is usually framed. RDS for PostgreSQL is closer to
upstream PostgreSQL than Aurora in storage and replication architecture, so more
of the PostgreSQL documentation applies to it directly. It is not upstream
PostgreSQL. There is no superuser, no host access, no arbitrary extension install
and no unrestricted background worker on either service.

## Cost, on both sides

The framing "RDS is predictable, Aurora is not" is too simple in both halves.

On the Aurora side, Standard charges per I/O request and I/O-Optimized does not.
AWS's guidance, read on 2026-09-19, from
[its own article on estimating the saving](https://aws.amazon.com/blogs/database/estimate-cost-savings-for-the-amazon-aurora-i-o-optimized-feature-using-amazon-cloudwatch/),
is that "cost savings provided by Aurora I/O-Optimized can be significant when I/O
spending accounts for 25% or more of total Aurora database spending". The same
article estimates that in advance from CloudWatch: "You can use CloudWatch metrics
to get storage and I/O usage details for your Aurora cluster and generate cost
estimates", using `VolumeReadIOPs`, `VolumeWriteIOPs` and `VolumeBytesUsed`. It
can therefore be modelled before you move, then checked against the first real
bill.

On the RDS side, the bill is not one number either. Storage type and size,
provisioned IOPS and throughput on gp3, the Multi-AZ topology, external read
replicas, backup retention beyond the free allowance, cross-AZ data transfer,
reservations and savings plans, and Region all move it.

The gp3 threshold is the one that surprises people. Read on 2026-09-19, the
[Amazon RDS DB instance storage page](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
says: "On gp3 storage volumes, Amazon RDS provides a baseline storage performance
of 3000 IOPS and 125 MiB/s." It then says the baseline rises at a size threshold:
"when the storage size for gp3 volumes reaches a certain threshold, the baseline
storage performance increases. This is because of *volume striping*, where the
storage uses four volumes instead of one."

The
[same page](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html),
read on 2026-09-19, says the AWS threshold for PostgreSQL is 400 GiB, and does
not vary by PostgreSQL version: 20 to 399 GiB gets
a 3,000 IOPS and 125 MiB/s baseline with provisioned IOPS "Not applicable", and
400 GiB and above gets 12,000 IOPS and 500 MiB/s with 12,000 to 64,000 IOPS
provisionable. So below the threshold you cannot buy more, and crossing it is a
step change in performance that arrives with a storage resize. It is the kind of
thing people attribute to the engine.

Model compute, storage, I/O, high-availability readers, backup, disaster recovery
and network on both sides, rather than comparing the Aurora I/O line item with an
RDS instance price.

## Availability is a property of your application

AWS's failover number is database promotion. Your users experience application
recovery, which is longer and which you have to measure. The requirements:

- Connect through the writer and reader endpoints, never an instance endpoint,
  and understand the DNS behaviour in front of them.
- Bound connection pool lifetime and test what your pool does with a stale
  connection.
- Retry only idempotent operations. In-flight transactions will fail.
- Test the cases separately: planned maintenance, writer loss, reader loss, AZ
  impairment, and a point-in-time restore.
- Write the target down as an objective in your own words, for example that the
  application resumes successful writes within ninety seconds of writer loss,
  and test against it. That is a number you choose, not one anybody measured
  for you.

## When RDS is the better answer

**Your database fits.** The 64 TiB that the
[RDS storage page](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
gives for most PostgreSQL DB instance classes, read on 2026-09-19 and stated for
every PostgreSQL version rather than per version, is a large database. Capacity
headroom is not the
whole storage question, though: growth behaviour, durability topology and I/O
profile still differ well below the ceiling, so decide on those rather than on
whether you will hit a number.

**A minute of failover is acceptable**, confirmed by whoever owns the
application rather than assumed.

**You need something Aurora does not carry.** An extension, a parameter, a
replication workflow, a version on a schedule that suits you.

**Predictable cost matters more to you than elasticity**, and you have modelled
both sides rather than comparing an instance price to an I/O line.

## When Aurora is worth it

**Storage growth you cannot predict**, because the volume grows and shrinks
without a resize operation.

**Failover in seconds rather than a minute**, when you have checked that a
minute is too long.

**Read scaling past what a Multi-AZ DB cluster's two readers give you**, up to
15 replicas on the same volume.

**Genuinely intermittent workloads**, where Aurora serverless scaling to zero is
the feature. For steady traffic it is not cheaper.

## Why does AI-generated code cause this?

Ask for "a production-ready Postgres on AWS" and the generated Terraform is
Aurora, with a writer and two readers, because that is the shape of the reference
architecture. It is a reasonable default and it is not a decision. The RDS
Multi-AZ DB cluster almost never appears, which is why so many teams believe
readable high-availability replicas are an Aurora-only feature. The cost
difference shows up a month later, attached to a workload that never needed the
storage layer it is paying for.

## How do I decide?

1. **Build the compatibility checklist first.** If something on it is missing
   from Aurora's matrix, the rest of the comparison is academic.
2. **Find the dominant bottleneck per workload class.** Capture plans and buffers
   for the slow queries, then wait events, then latency and queue depth. Do not
   stop at a cache hit rate.
3. **Get the failover objective from the application owner**, as a number, for
   application recovery rather than database promotion.
4. **Write down the staleness rule** for each read path: writer endpoint, reader
   endpoint, or application-level routing.
5. **Model the full cost on both sides**, including the I/O estimate from
   CloudWatch if Aurora is in the running.

## How DBGorilla helps

DBGorilla connects read-only to PostgreSQL on either service and gives those
questions data instead of a guess: workload by query fingerprint with duration,
invocations and buffer counters, so you can see whether one query or many are
the problem; metric time series with computed rates, including index and table
sizes; topology snapshots of tables and indexes with their history; and, when
commands are enabled in the collector configuration, EXPLAIN against the live
database.
All of that was verified in a live session.
[Get started free →](https://app.dbgorilla.com/signup)