A client's Postgres database had been self-managed on a GCP Compute Engine instance since we built it, with us handling backups, patching, and replication configuration by hand. This year we migrated it to Cloud SQL, Google's managed Postgres offering, mainly to free up the ongoing operational time that self-managing it required.
What moved from us to Google
- Automated point-in-time backups, replacing a cron-scheduled backup script we had written and maintained ourselves, along with the periodic manual verification that script's output was actually restorable
- One-click read replica setup, replacing a process that previously required us to manually configure Postgres streaming replication and monitor replication lag ourselves
- Automated minor-version patching on a schedule we control, replacing a manual process of testing and applying Postgres patches during a planned maintenance window
The operational relief, in concrete terms
The clearest, most immediate win was backup verification. Our old manual process involved periodically restoring a backup to a scratch instance to confirm it actually worked, a task that was important but tedious enough that it occasionally slipped by a few days past its intended schedule under competing deadline pressure. Cloud SQL's automated backups come with built-in restore testing as part of the managed service, removing a task that depended on human discipline under exactly the kind of workload pressure that human discipline tends to fail under.
Working through the cost comparison honestly
The monthly cost is meaningfully higher than the equivalent bare Compute Engine instance, and we did not want to recommend this migration to the client on vibes alone, so we built an actual cost comparison: the Cloud SQL instance's monthly bill against the bare Compute Engine instance's monthly bill, plus an estimate of the engineering hours we used to spend on backup verification, patch scheduling, and replication monitoring, valued at our normal billing rate rather than treated as free internal overhead. Once we accounted for the engineering hours no longer spent on backup verification and patch scheduling, it was a clear net positive for this client, and we are recommending the same move for a couple of others still self-managing Postgres.
The migration itself, and where we hit friction
Migrating the actual data used Cloud SQL's built-in migration tooling, which handles an initial bulk data copy followed by ongoing replication from the source database until we were ready to cut over, keeping downtime to a single short connection-string swap rather than a lengthy offline migration window. The one place we hit real friction was a set of Postgres extensions the client's application depended on that Cloud SQL's managed environment does not support, since Google restricts which extensions can run on its managed instances for security and operational reasons. We had to rework two queries that depended on one particular unsupported extension, finding a functionally equivalent approach using only extensions Cloud SQL does support, which took a few extra days of migration planning we had not originally budgeted for.
High availability, and what we no longer have to build ourselves
Failover for high availability, previously something we would have had to build and test ourselves — configuring a standby replica, writing and testing our own failover automation, periodically running failover drills to confirm it actually works under pressure — is now a configuration checkbox backed by Google's own tested failover mechanism. We still run periodic failover drills against the client's Cloud SQL instance, since we do not consider a managed service's failover claims a substitute for actually verifying it works for this specific client's configuration, but the mechanism itself is no longer something we had to design and build from scratch.
Monitoring changes that came along with the migration
Cloud SQL integrates directly with Google Cloud's monitoring tools, giving us built-in dashboards for connection count, query performance, and replication lag with no separate monitoring agent to install and maintain the way our self-managed instance required. We did have to rebuild a couple of custom alerting rules that our old self-managed monitoring setup had accumulated over time, translating them into Cloud SQL's monitoring metrics rather than assuming the managed service's default alerts alone would catch everything the client's application specifically needed watched.
An incident from before the migration that this setup would have prevented
Before this migration, a botched manual patch application on the self-managed instance once caused an unplanned outage lasting the better part of an afternoon, when a Postgres minor version update introduced a subtle configuration incompatibility with a setting the client's application depended on, and diagnosing it under live-outage pressure took considerably longer than a calm, planned review would have. Cloud SQL's managed patching process tests updates against Google's own infrastructure before rolling them out broadly, which would not have guaranteed catching this specific client-side configuration interaction, but the incident was a real part of what motivated pursuing this migration in the first place rather than continuing to accept that risk indefinitely.
What we would check for extension compatibility earlier on a future migration
The unsupported Postgres extension we discovered partway through this migration is exactly the kind of thing we should have checked during initial migration scoping rather than discovering once the migration was already underway. We have since added a specific extension-compatibility check to our standard Cloud SQL migration checklist, run against Google's current list of supported extensions before any migration timeline gets committed to a client, specifically to avoid repeating the few days of unplanned rework this project needed partway through.
An analytics use case the new read replica setup enabled
A few months after the migration, the client's own team asked for direct database access for an internal analytics dashboard, a request we would have been more hesitant to grant against the old self-managed primary database, given the risk of a poorly written analytics query affecting production application performance. Cloud SQL's one-click read replica setup let us spin up a dedicated replica for exactly this purpose in a few minutes, giving the analytics dashboard its own isolated copy of the data to query against with zero risk to the primary database's performance, a capability that existed in principle under the old self-managed setup too but would have taken real configuration effort to stand up rather than a few clicks.
One more genuine limitation worth naming honestly
Cloud SQL's restricted extension support, which cost us a few days of rework on this migration, is not a one-time cost that disappears afterward — it is an ongoing constraint on what the client's application can ever depend on going forward, since any future feature requiring a Postgres extension Google does not support on its managed service would face the same problem again. We flagged this explicitly to the client as a real, lasting tradeoff of the managed service rather than a one-off migration inconvenience, so that a future feature decision is made with that constraint in mind rather than being surprised by it again later.
Results
Automated point-in-time backups and one-click read replica setup replaced work we used to do manually and occasionally got wrong under time pressure. The monthly cost increase, once weighed honestly against the engineering time no longer spent on operational upkeep, was a clear net positive for this client, and the migration experience — extension compatibility issues aside — has been smooth enough that we now default new Postgres-backed GCP projects straight to Cloud SQL rather than self-hosting at all.
A follow-up conversation about vendor lock-in the client raised
A few weeks after the migration, the client's own leadership raised a fair concern about vendor lock-in, worried that moving fully onto a Google-specific managed service made a future move to a different cloud provider harder than it would be with a self-managed, more portable Postgres instance. We addressed this directly: Cloud SQL is still standard Postgres under the hood, and the client's actual application code has no Google-specific dependencies baked into it, so a future migration to a different provider's managed Postgres offering, or back to self-managed, would look much like this migration in reverse rather than a from-scratch rewrite. That said, we were honest that some Cloud SQL-specific operational tooling and automation would need rebuilding elsewhere, which is a real, if modest, switching cost worth naming rather than glossing over. We are keeping a running internal note of every Cloud SQL-specific operational detail we would need to unwind in a future migration, specifically so that if this client's lock-in concern ever needs a concrete answer rather than a general reassurance, we already have one ready.