IT Consultant Software Engineer Philippines
SELF-HOSTED VS MANAG May 9, 2026

The Managed Postgres Trap: Why Self-Hosting Can Be Cheaper, Saner, and Faster

We hit an AWS bill for $18,000 in a single month for a single Postgres database. This wasn't a massive data warehouse, just a core application database with about 800GB of data. The problem wasn't our queries, it was the "managed" solution actively working against us, pushing us into an expensive co

The Managed Postgres Trap: Why Self-Hosting Can Be Cheaper, Saner, and Faster

We hit an AWS bill for $18,000 in a single month for a single Postgres database. This wasn't a massive data warehouse, just a core application database with about 800GB of data. The problem wasn't our queries, it was the "managed" solution actively working against us, pushing us into an expensive corner we couldn't escape without a full migration.

Why this matters in 2026

Every engineering leader I talk to is scrutinizing cloud costs. We're past the "move everything to the cloud" euphoria, now we're paying the bills. Database services, especially Postgres, are often the quiet culprits, eating up a disproportionate chunk of the budget while simultaneously limiting your operational flexibility. As application demands grow, and the need for specialized database features increases, the trade-offs between convenience and control become starker, impacting both your bottom line and your team's ability to innovate.

Three things I learned shipping this in production

The real cost of "managed" isn't just the bill

When we started, AWS RDS seemed like the obvious choice for our core application database. It was "managed," which promised less operational overhead. We picked a db.r5.xlarge instance, 1TB of GP2 storage, running PostgreSQL 14.3. The initial bill was manageable, around $800 a month. Then traffic grew. Our application started seeing Latency high alerts, database connections would occasionally drop, and our response times spiked. The RDS monitoring showed our GP2 storage was consistently hitting its burst IOPS limits, and eventually, its baseline IOPS. To fix this, AWS's recommendation was to provision more storage to get more IOPS, or switch to Provisioned IOPS (PIOPS).

We scaled the storage to 3TB just to get the required IOPS throughput, even though we were only using 800GB of disk space. Our bill for that single database jumped to over $4,000. When that still wasn't enough, we had to switch to PIOPS, which instantly pushed the cost to $12,000 a month for 20,000 PIOPS. This was for a database that, on a raw VM, would have cost us maybe $1,500 a month for comparable CPU/RAM and NVMe storage. The problem wasn't just the dollar figure, it was the lack of transparency. We couldn't see why we were hitting limits, we just saw the black box performance metrics and a single knob to turn: "spend more money." We also couldn't use specific Postgres extensions like pg_partman to manage our large tables because RDS didn't support it, forcing us to build complex application-level partitioning logic. This isn't "managed," it's "constrained."

Self-hosting isn't just about VMs, it's about tooling

The idea of self-hosting used to conjure images of sysadmins hand-crafting shell scripts and praying to the data gods. That's not how it works anymore. The operational overhead of a well-architected self-hosted Postgres cluster, especially for a single, critical database, can be less than dealing with the quirks of a managed service. The secret is open-source tooling.

We migrated our problematic database to a self-hosted cluster on EC2 instances. For high availability, we deployed Patroni 2.1.5. Patroni orchestrates a Postgres cluster, handles failovers, and manages replication with tools like pg_rewind. It uses a distributed consensus store, like etcd or ZooKeeper, to manage state. For backups and point-in-time recovery (PITR), we used pgBackRest 2.45, configured to ship WAL archives to S3. Monitoring was handled by Prometheus 2.37.1 and Grafana 9.1.5, pulling metrics directly from pg_stat_statements and node exporter.

This stack, running on m5.xlarge instances with local NVMe disks, cost us about $1,800 a month. That's a 70% reduction from the PIOPS bill. The operational lift was front-loaded: setting up Patroni and pgBackRest takes a few days. Once it was running, it was stable. Failovers were automatic, backups were verified, and we had granular control over every Postgres parameter. We saw exactly what was happening with disk IO, memory, and CPU, because we owned the entire stack.

Here's a snippet of a simplified patroni.yml that shows how straightforward the HA configuration can be:

scope: my_app_cluster
namespace: /service/
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB
postgresql:
  parameters:
    max_connections: 200
    shared_buffers: 1GB
    wal_level: replica
  pg_hba:
    - host all all 0.0.0.0/0 md5
  create_replica_methods:
    - basebackup
restapi:
  listen: 0.0.0.0:8008
  connect_address: 10.0.0.10:8008 # Replace with actual IP
log:
  level: INFO
This configuration defines the cluster name, basic timeouts, and key Postgres parameters. Patroni uses this to manage the cluster's lifecycle, including promoting a replica if the primary fails. It gives you control without forcing you to write bespoke scripts.

You pay for control, one way or another

The biggest argument for self-hosting is control, and it's not just about cost. It's about being able to tune your database for your specific workload, use any extension you need, and debug problems without guessing.

We had a financial reporting service that generated massive, temporary tables for complex calculations. On RDS, these queries would often fail with out of memory errors or simply run for hours. The work_mem parameter, which controls the amount of memory used by internal sort operations and hash tables before writing to disk, was limited by RDS. We couldn't set it high enough to prevent disk spills for these specific operations without affecting other parts of the database.

On our self-hosted Postgres 15.2 instance, we could dynamically adjust work_mem for specific sessions or even globally for a short period. We also enabled pg_stat_statements, a powerful extension that tracks execution statistics of all SQL statements. This let us pinpoint slow queries, identify inefficient indexing, and see exactly where our resources were going. RDS offers some of this, but often in a simplified, aggregated view. We needed the raw data.

Being able to load extensions like pg_partman for automatic table partitioning on large time-series tables, or pg_cron for scheduling database jobs directly within Postgres, greatly simplified our application logic. We could offload these tasks to the database, where they belonged, rather than having external cron jobs or application workers manage them. This reduced complexity in our application code and made database operations more robust.

Here's a simple query using pg_stat_statements to find the slowest queries, which is invaluable for performance tuning:

SELECT
  query,
  calls,
  total_exec_time,
  mean_exec_time,
  rows,
  round(100.0 * shared_blks_hit / (shared_blks_hit + shared_blks_read + 1), 2) AS hit_ratio
FROM
  pg_stat_statements
ORDER BY
  total_exec_time DESC
LIMIT 10;
This level of detail is harder to get, or sometimes impossible to act on, in a managed service environment. You own the database, you own the data, and you own the operational destiny.

What I would do differently if I started today

I would start with self-hosted Postgres for any application where the database is a core component or is expected to scale beyond a trivial workload. The default should not be managed. The mental model of "just spin up RDS" is a trap for anything that isn't a simple proof-of-concept or a non-critical backend service. The initial setup cost for a self-hosted cluster with Patroni and pgBackRest is quickly recouped in cost savings and operational sanity. The only exception would be if I had zero database expertise on the team, and even then, I would hire that expertise before I’d blindly commit to a managed service for a critical system.

What this looks like for your team

1. Audit your current database costs and performance bottlenecks: Look beyond the monthly bill. Are you over-provisioning storage for IOPS on RDS? Are you hitting CPU or memory limits that are hard to diagnose? Identify specific pain points where managed services are constraining you. 2. Prototype a self-hosted cluster for a non-critical service: Pick a less critical application database. Set up a two-node Patroni cluster with pgBackRest for backups on commodity VMs. Instrument it with Prometheus and Grafana. Get comfortable with the tools and the operational model before considering a migration for your core databases. 3. Calculate the true TCO (Total Cost of Ownership) for both options: Include the cost of developer time spent working around managed service limitations, the cost of scaling issues, and the direct infrastructure costs. You might be surprised at how quickly the "managed" premium outweighs the perceived operational savings.

I write about engineering decisions and production systems at devwithzach.com — drop me a line if any of this rings true.

Need IT Consulting or Software Development?

Let's talk about your project. Free initial consultation.

Book Free Consultation ↗