Google put AlloyDB into preview last month, AWS made Aurora Serverless v2 generally available three weeks before that, and Azure's Flexible Server sits beside them, all three Postgres compatible. This is about what that label still covers and what it stopped covering: the wire protocol and the SQL dialect travel, and storage, failover and the extension list do not. Portability turns out to be a property of the subset you agree on day one not to use.
23 June 2022·8 min read·datacloud
Postgres compatible is a real claim and it is worth saying what it buys, because it is not nothing. Your driver connects. psql connects. The SQL you wrote runs, the types behave, the isolation levels mean what they mean, your ORM's migration files apply without edits, and the query planner is recognizably the one you have been reading EXPLAIN output from for a decade. That is genuinely most of the work of a database, and it is why the label sells.
What it does not cover is everything you learn about a database after it is in production. How long a failover takes and what the application sees during one. Whether a read replica can be stale, and by how much. Which extensions you may install, at which versions, and who decides. What a restore actually restores, and how you get the data out if you want to leave. None of those are in the dialect. All of them are in your incident reviews.
That gap was small enough to ignore for years, when a managed offering was mostly Postgres on a virtual machine with the backup script written for you. Two announcements in the last two months, from two different clouds, are both explicitly about the parts underneath the protocol.
So the useful reframing is that portability is not a property of the database you picked. It is a property of the subset of it you agreed not to use, decided on the day you started rather than the day the contract comes up, and paid for continuously in features you can see and are choosing to leave alone.
Aurora moved the redo log into the storage service. The SIGMOD 2017 paper from the Aurora team is unusually candid about the reasoning: in a cloud database the bottleneck has moved to the network between the database tier and the storage tier, so Aurora sends redo log records instead of pages and lets the storage service materialize them. The cluster volume holds six copies across three availability zones, with a write quorum of four and a read quorum of three, which is what lets it lose a whole zone plus one more node and keep going.
AlloyDB disaggregates and adds a column store. Google's preview announcement of 11 May describes a storage service written specifically for Postgres, compute and storage separated at every layer, data tiered automatically across caches, and a vectorized columnar accelerator that keeps a second in-memory representation of your data in column form so scans and aggregations do not have to walk row pages. That is two engines over one dataset, which is a database design decision and not a hosting decision.
Azure kept the engine on a disk, deliberately. Flexible Server runs the Postgres engine on a Linux virtual machine with the data files and write-ahead log on managed premium disks, and the storage layer keeps three synchronous copies within the zone. There is no rewritten storage engine and no log service. It is the closest of the three to stock Postgres, which is a design position rather than an absence of one: less of it is novel, so less of it is unfamiliar when something goes wrong at two in the morning.
Google's announcement says AlloyDB is four times faster than standard PostgreSQL for transactional workloads, up to a hundred times faster for analytical queries, and twice as fast as Amazon's comparable service on transactions. The Aurora paper reports sustaining thirty-five times more transactions than mirrored MySQL over a thirty minute run, with the database node issuing 7.7 times fewer I/Os per transaction. Every one of those figures was produced by the company selling the thing, on a workload it chose.
That does not make them false. It makes them answers to a question you did not ask. A hundred times faster on analytical queries is a statement about the columnar accelerator on scan-heavy work, and it tells you nothing about the workload you actually run, which is probably neither of the two shapes being compared. The number worth extracting is not the multiple. It is the architecture the multiple implies, because that is the part you will be operating.
The honest version of the comparison is that all three are faster than an untuned Postgres on a small instance, which is the baseline most of these numbers are quietly drawn against, and that the difference between them for your workload is unknowable until you run your workload. What is knowable in advance is what each one does when a node dies, and that is a much better basis for a decision.
Aurora promotes a reader that already shares the volume. Because every instance in the cluster reads the same storage, a failover is a promotion rather than a data movement. AWS documents service typically restored in less than 60 seconds, and often less than 30, when the cluster has at least one reader in another zone. With no reader at all it recreates the primary instead, and documents that as typically under 10 minutes, which is a twentyfold difference produced entirely by whether somebody provisioned a second instance.
Azure keeps a warm standby that nobody can read. With high availability enabled, Flexible Server maintains a standby of the same size, and the primary streams write-ahead log to it and does not acknowledge a commit until the standby has durably stored it. That gives zero data loss on a zone failure and costs a little commit latency on every write. The standby serves no client traffic at all, so the second machine you are paying for adds no read capacity, and documented failover is 60 to 120 seconds.
Some replicas are stale and some are not readable at all. Aurora readers are updated asynchronously and can serve slightly stale data, which is fine until somebody writes a row and reads it back through the reader endpoint. Azure's in-region standby is not readable at all, and its cross-region replicas are asynchronous with lag the documentation describes as typically several minutes. Those are three different contracts for the phrase "read replica", and application code written against one of them is wrong against the others in ways that pass every test.
And the endpoint is part of the design. Aurora gives you a cluster endpoint that always points at the current writer, Azure gives you virtual endpoints that follow a promotion, and both exist because clients cache DNS and reconnect badly. Whichever you get, the failover time your users experience is the documented number plus your driver's connection timeout plus whatever your pooler does with a connection that has quietly become read-only. That last term is usually the largest and it is the only one you control.
It went generally available on the twenty-first of April. The unit is an Aurora capacity unit, roughly a bundle of CPU and memory, and the service adjusts in increments as small as 0.5 ACU between a floor of 0.5 and a ceiling of 128. The important word in the announcement is "in place": it grows the underlying instance without failing over to a new one, in milliseconds, which is what the first version could not do.
That distinction is the whole feature. The earlier serverless design scaled by finding a moment when it was safe to swap you onto a different instance, which meant scaling was an event with a cost, and which is why a lot of teams tried it once and went back to provisioned. Scaling that does not interrupt connections is a different product wearing the same name, and it also mixes with provisioned instances inside one cluster, so a writer can be fixed while readers float.
The consequence is that capacity stops being a number in your infrastructure code and becomes a runtime property with a floor and a ceiling. Everything sized against instance class moves with it. Connection limits scale with capacity, so a pool sized for the ceiling will overwhelm the floor, and a floor set too low means a workload that arrives suddenly is served by the smallest configuration you allowed while it climbs.
None of that has an equivalent on the other two, which is the point. Azure's compute tiers are chosen and changed as a deliberate operation, and AlloyDB was six weeks old and in preview as I write this. A design that assumes elastic capacity underneath is a design that has picked a provider, and it will have picked it in a way that does not show up anywhere in the schema.
You are not a superuser and you are not going to be. On a managed service you get a role with elevated but bounded privileges rather than superuser, because superuser on a machine you do not own is a hole in the provider's isolation model. That is correct of them and it is also the root of everything else in this section: anything in Postgres that requires superuser is now a request, and a request is answered by a product manager on a roadmap rather than by you on a Tuesday.
The preload list is the provider's, not yours. Extensions that hook the engine at startup have to be named in shared_preload_libraries, which means they require a restart and, on a managed service, permission. So the set of extensions that can exist on your database is fixed by somebody else's allow list, at versions they pin, upgraded on their schedule. Two providers can both offer an extension and offer different major versions of it, which is a difference your queries can see.
An extension in your schema is not a dependency you can drop. A library is easy to leave behind. An extension that supplies a column type, an index method or a constraint is written into your data, and removing it means a migration of every table that touched it. The moment a spatial type or a vendor-specific index is in a production table, the exit cost stopped being a code change and became a data change, and the estimate for it went up by an order of magnitude without anyone noticing.
So the allow list is the compatibility boundary. Not the SQL standard, not the version number, not the marketing. The honest test for portability is to take the intersection of the three published extension lists, write that intersection down, and treat everything outside it as a decision that costs money to reverse. Doing that on day one is a paragraph in a design document. Doing it at renewal time is a project with a name.
Write down which behaviors you are depending on, in the specific rather than the general. Not "we need high availability" but "we assume a failover completes inside a minute and that in-flight transactions fail rather than hang". Not "we use read replicas" but "this endpoint tolerates data that is a few seconds old and this one does not". Those sentences are portable across providers in a way that a configuration file is not, and they are testable.
Keep an exit that is exercised rather than described. Logical replication out to a plain Postgres you run yourself is the one path all three support, and it either works this quarter or it does not; the way to know is to do it, once, and time it. A documented migration plan that has never been run is a piece of writing. It becomes an engineering estimate the first time somebody actually completes it.
And then decide, honestly, whether you want portability at all. There is a perfectly respectable position that says you picked a cloud, you are going to use the good parts, and the columnar accelerator or the quorum storage layer is worth being unable to leave for. I have less patience for the third option, which is the common one: believing you are portable because the label says Postgres, while shipping features every quarter that quietly make it untrue.
I should concede the size of this. For a great many workloads none of it matters, because the workload is a few hundred gigabytes of straightforward relational data with modest traffic, and every one of these services will run it well and let you leave with a dump file on a Sunday afternoon. The divergence bites at the edges: heavy write volume, tight recovery objectives, an extension doing real work, a schema shaped around one engine's strengths.
What I keep coming back to is that the compatibility label is measured at the connection and the cost is paid everywhere else. The protocol is the narrow part of the interface and the operational behavior is the wide part, and the wide part is the one nobody writes into a contract. Three services answer to the same name, speak the same dialect, and would each require a different runbook, a different failover drill and a different set of forbidden features. Only one of those facts fits on a slide.