Skip to content
Aurora
Sub-page 1 of 8
Core one-way-door · Free

Choosing MySQL- or PostgreSQL-Compatibility Before Backtrack and Autovacuum Make the Choice for You

Decision drivers
  • Which engine (MySQL or PostgreSQL) the team already runs and has tooling/expertise for
  • Whether the workload includes long-running analytics/reporting queries against read replicas
  • Whether the recovery plan assumes Backtrack (MySQL-only)
  • Dependence on any PostgreSQL extension not on Aurora's supported list

Production premise

In practice, you pick the engine your team already runs. Tools, ORMs, backups, and the people themselves are already tied to MySQL or PostgreSQL. Fighting that rarely pays.

But "compatible" is not "identical," and one difference shows up before you've written a single query: Aurora PostgreSQL does not run vacuum the way stock PostgreSQL runs it.

Quick background, if you haven't lived in this corner of Postgres: every delete or update leaves dead rows behind, and vacuum is the background process that cleans them up. Vacuum also has a deadline to meet. PostgreSQL numbers its transactions with a counter that eventually wraps around, and every table must be fully vacuumed before its counter gets close to the limit (autovacuum_freeze_max_age) — or old data becomes unreadable.

Aurora adds adaptive autovacuum (rds.adaptive_autovacuum, on by default). As a table's transaction counter approaches that limit, Aurora automatically turns vacuum up. Stock PostgreSQL will not do that on its own; you'd have to notice the climb and tune by hand.

Failure mode & inflection point

Adaptive autovacuum covers the wraparound deadline. It does nothing for a second problem, and this one exists only because of how Aurora is built.

In ordinary PostgreSQL replication, a replica replays the primary's changes onto its own copy of the data, and the two copies mostly stay out of each other's way. Aurora does not work that way. The writer and all the read replicas share one storage layer. That is why Aurora replicas are quick to create and don't fall behind on replay — and it is also why a query running on a reader can reach back and stall cleanup on the writer.

The chain goes like this. A long-running query on a reader still needs the old versions of the rows it is reading, so the replica tells the writer to keep them. PostgreSQL calls this hot_standby_feedback. In standard RDS PostgreSQL it is a setting you can turn on or off. In Aurora PostgreSQL it is on by default and cannot be turned off. The writer's vacuum must now leave those old rows alone, so cleanup on the writer stalls for as long as the query on the reader runs.

You notice it like this: vacuum looks like it is running, but the CloudWatch metric MaximumUsedTransactionIDs keeps climbing toward your autovacuum_freeze_max_age value and will not come down. When you see that, do not start tuning vacuum on the writer. Look for long-running queries, or sessions sitting idle inside a transaction, on the readers. That is the actual blocker.

One more asymmetry, on the MySQL side: Backtrack — Aurora MySQL's ability to rewind the whole cluster back to a moment in the past (page 7) — does not exist for PostgreSQL at all. If your recovery runbook assumes it does, you will find that out in the middle of an incident.

Production guardrail

Building something genuinely new, with no existing stack to match? Default to PostgreSQL. On Aurora specifically, the PostgreSQL side gets the more active feature development, the broader extension ecosystem (pgvector, if anything you build touches embeddings), and type and constraint checking that rejects bad data loudly — MySQL would accept the same data silently and cost you later.

Choose MySQL when you can name the reason: your team already runs MySQL tooling, ORMs, or a CMS that requires it (WordPress and Drupal both do); you need Backtrack's fast rewind (page 7) and restoring a snapshot is not fast enough for you; or your team's MySQL experience is deep enough that switching would cost more in ramp-up than PostgreSQL buys in features.

If you do land on PostgreSQL, set one alarm before anything else: watch MaximumUsedTransactionIDs against your autovacuum_freeze_max_age value. When it starts climbing, check the readers first — a long-running query or an idle-in-transaction session there is the likely cause. Since you cannot turn hot_standby_feedback off in Aurora, the fix is to cap how long analytics and reporting queries are allowed to run on replicas. Tuning vacuum on the writer will not help.

Last check before migration planning: list every PostgreSQL extension your team depends on and check it against Aurora's supported list. Aurora PostgreSQL has historically been slower than vanilla PostgreSQL to add support for newer extensions.