Five years ago, running production Postgres on Kubernetes was a niche choice that came with operational pain. Today it is a real option that several large companies have committed to. The tooling is mature; the failure modes are tractable; the cost story is competitive with managed services for many workloads.
It is also still more work than RDS. Here is the framing I use when teams ask whether to run Postgres on Kubernetes.
The case for Postgres on Kubernetes
The upsides:
- Cost: a self-hosted Postgres on K8s on EC2 reserved instances can be 50-70% cheaper than RDS for the same instance size.
- Control: you control the version, extensions, configuration, monitoring stack. No managed-service quirks.
- Multi-cloud or on-prem: same operator works on GKE, AKS, EKS, or bare metal.
- Backup and DR: usually integrated with object storage, more flexible than managed defaults.
The case against
- Operational burden: someone on your team owns the database. Upgrades, patches, vacuum tuning, capacity planning. Managed services handle most of this for you.
- Failover: K8s operators handle failover but the integration with application reconnection is something you have to test.
- Storage: persistent volumes on K8s have come a long way but are still trickier than RDS's instance-attached EBS.
- Disk performance: not all CSI drivers have RDS-grade performance. Test on your specific stack.
My default recommendation: if you have a dedicated DBA-style engineer (or a team that can absorb that role), Postgres on K8s is viable. If you do not, RDS is worth its premium.
The operators worth considering
Three mature options:
CloudNativePG — relatively young, well-designed, growing fast. Strong opinions about the right way to run Postgres on K8s. The recommended choice for new deployments today.
Crunchy Postgres Operator (PGO) — mature, conservative, owned by Crunchy Data. Excellent backup story (pgBackRest integration). Good choice for teams that value stability.
Zalando Postgres Operator — battle-tested at Zalando's scale. More opinionated than the others, slightly more complex. Use it if you want what Zalando uses.
I have shipped CloudNativePG and PGO. Both work. The differences are in operational ergonomics rather than fundamental capability.
Storage decisions
The single most important Kubernetes-Postgres decision is storage. Three patterns:
EBS (or equivalent block storage): a PersistentVolume backed by EBS. Pod restarts attach to the same volume; data survives.
- Pros: durable, reasonable performance, well-understood.
- Cons: latency higher than instance-attached storage. IOPS can be costly at scale.
Local SSD with replication: each pod uses a local NVMe SSD. The operator maintains synchronous replicas to handle node failure.
- Pros: the highest performance available — local NVMe is hard to beat.
- Cons: pod restarts on a different node lose data; the operator has to handle re-replication. More complex but viable.
Distributed block storage (Longhorn, OpenEBS, Rook): replicated block storage on the K8s cluster.
- Pros: pod restarts find their data anywhere in the cluster.
- Cons: performance varies wildly. I have seen production workloads that worked great and ones that suffered. Test on your hardware before committing.
For most workloads on cloud K8s, EBS-style PersistentVolumes are the right starting point. For very high-performance workloads, local SSDs with replication.
Backup and PITR
The operator should integrate with an object store for backups. The standard setup:
- Continuous WAL archiving to S3 (or compatible).
- Daily base backups via
pg_basebackup. - 7-30 days retention.
- Point-in-time recovery is a single CRD field away.
All three operators above support this out of the box. The pattern is mature.
The one thing to verify yourself: do a restore drill. The operator's psql integration is great for routine ops; the actual end-to-end restore needs to be tested manually before you trust it.
Failover
K8s operators handle pod-level failures by:
- Detecting the failed pod.
- Promoting a replica.
- Updating the service endpoint to point at the new primary.
- Optionally re-creating the failed pod as a new replica.
The trick is in step 3. Application connections to the service experience a brief disconnect during the switch. Your application's reconnect logic has to handle this gracefully.
Time to recover: typically 30-90 seconds for a planned failover, longer for unexpected node loss (the operator has to detect the failure first).
Resource sizing
Some rough guidelines for production workloads:
- Pod resources: do not skimp. A Postgres pod that gets OOM-killed loses data integrity.
- Memory request = (shared_buffers + work_mem * estimated concurrent ops) × 1.5
- CPU request = real production load + 50%
- Persistent Volume: 2-3x current data size. Resizing PVs on K8s is possible but operationally annoying.
- Backup space: WAL accumulation can be significant. Reserve 2x daily WAL volume for a buffer.
Monitoring
The Postgres metrics you want exported to your observability stack:
pg_exporter(the Prometheus exporter) for query rates, replication lag, connection count, vacuum stats.- Operator-level metrics: cluster state, failover events, backup success.
- Storage metrics: PV usage, IOPS, latency.
For query-level diagnostics, the application-side approach (sending traces) plus pg_stat_statements is the standard.
A common failure mode: the noisy neighbor
Running Postgres pods on shared K8s nodes where other workloads also run is asking for trouble. Postgres expects predictable I/O and CPU. A noisy neighbor pod with a CPU-bound workload can starve Postgres of cycles, causing latency spikes that look like database problems.
The fix: dedicated node pools for Postgres. Use taints and tolerations to keep other workloads off them. The cost is some idle capacity; the benefit is predictable performance.
What I tell teams considering this
The checklist:
- Do you have someone who will own this operationally? If no → use RDS.
- Is your data set large enough that the cost savings actually matter? (Below ~$1k/month on RDS, the savings are smaller than the operational cost.)
- Are you comfortable doing your own backup drills, vacuum tuning, version upgrades?
- Is your application's reconnect logic robust enough to handle 30-second failovers?
All yes → Postgres on K8s is a good fit. Any no → managed.
A migration story
A team I worked with moved from RDS to CloudNativePG on EKS. The migration took six months including planning and testing. The first three months were operator setup, backup drills, failover testing. The next three were dual-running and cutover.
Post-migration:
- Cost dropped 60% (RDS bill of $14k/month became EKS + EBS + storage of $5.5k/month).
- Operational time increased: ~10 hours/month of dedicated DBA-style work.
- Latency improved slightly (instance-attached EBS gp3 vs RDS-managed io1).
- Failover events: two unplanned in 8 months, both handled cleanly.
The team was happy with the trade. The team that started the same migration two years earlier and abandoned it (because the operator was too immature then) was the source of half the lessons in this post.
Being on the second wave matters here. The first wave figured out what does not work. The second wave gets the benefit.