"Highly available PostgreSQL" usually means leader election, streaming replicas, automatic failover, health checks, and a lot of careful wiring. With the CYBERTEC PG Operator (CPO) it means a 14-line YAML file. Here's the whole thing, start to finish, on a laptop with minikube.
Every command and output below comes from a tutorial we ran end-to-end on a fresh minikube(Kubernetes 1.30, CPO 0.9.2, PostgreSQL 18.4).
minikube start -p cpo-deploy --driver=docker --cpus=2 --memory=4096
helm repo add cpo https://cybertec-postgresql.github.io/CYBERTEC-operator-tutorials
helm repo update cpo
kubectl create namespace cpo
helm install cpo cpo/postgres-operator -n cpo --version 0.9.2 \
--set configKubernetes.enable_pod_antiaffinity=false
kubectl -n cpo rollout status deploy/postgres-operator
One information worth knowing: enable_pod_antiaffinity=false flag. By default the operator spreads replicas across different Kubernetes nodes, exactly what you want in production. But minikube is a single node, so without this flag the replica would sit Pending forever. On a real multi-node cluster, leave anti-affinity on.
kubectl -n cpo apply -f - <<'EOF'
apiVersion: cpo.opensource.cybertec.at/v1
kind: postgresql
metadata:
name: pg-cluster
spec:
dockerImage: 'containers.cybertec.at/cybertec-pg-container/postgres:rocky9-18.4-1'
numberOfInstances: 2
postgresql:
version: '18'
resources:
requests: { cpu: 250m, memory: 1Gi }
limits: { cpu: '1', memory: 1Gi }
teamId: acid
volume:
size: 1Gi
EOF
numberOfInstances: 2 is the whole HA story: one leader, one streaming replica. Wait for them:
bash
kubectl -n cpo wait --for=condition=Ready pod \
-l cluster.cpo.opensource.cybertec.at/name=pg-cluster --timeout=360s
# pod/pg-cluster-0 condition met
# pod/pg-cluster-1 condition met
Ask Patroni, which runs inside every database pod, and what it sees:
kubectl -n cpo exec pg-cluster-0 -- patroniHigh availability is essential for logical replication environments, but until recently, failover could still leave subscribers disconnected from the replication slots they depend on. PostgreSQL 17 addressed this by introducing failover slot synchronization, allowing logical replication slots to be kept ready on standby servers and reducing the need for full subscriber resynchronization after promotion.
A production-ready approach to preventing duplicate leads and side effects with stable idempotency keys, PostgreSQL constraints, transactional outbox events, safe retries, and reconciliation.
A visitor fills out a form, presses Submit, and sees nothing happen. They press it again. The first request actually succeeded, but its response was delayed. The result can be two leads, two confirmation emails, two CRM updates, and two analytics events from one human action.
This is easy to dismiss as a frontend problem, but duplicate submission is a distributed-systems problem in miniature. Browsers retry, mobile connections fail after the server has committed, serverless functions time out, queues redeliver work, and reconciliation jobs intentionally retry failures. The server cannot infer whether two matching requests represent one action or two deliberate actions unless the client gives both attempts the same identity.
Disabling the button is still worthwhile. It improves the interface, stops impatient double-clicks, and tells the visitor that work is in progress. It does not protect the system from a refreshed page, a second browser tab, an automatic client retry, a proxy retry, a function timeout after commit, or a worker processing the same event twice.
The browser guard and the server guarantee solve different problems. Use both, but treat the database guarantee as the source of truth. Anything that depends only on React state disappears when the page reloads and can be bypassed by any direct API client.
For every logical submission, the client generates one unpredictable key. Every retry of that submission reuses the key. A genuinely new submission gets a new key. The server scopes the key to
[...]
A walkthrough of running OpenBao on Kubernetes with CloudNativePG as its PostgreSQL storage backend. Every layer of this stack is open source, with no vendor lock-in: Kubernetes and CloudNativePG are both CNCF projects, authenticated entirely over TLS client certificates via the 1.30 DatabaseRole CRD, with no passwords anywhere in the stack. Co-authored with Rob Kenefeck from ControlPlane.
Tool capabilities and edition boundaries described here are accurate as of publication. Both vendors move these lines; check current documentation before making decisions based on this post.
There’s a specific moment this post is written for: your pipeline runs flyway validate, everything is green, and you conclude that your database matches your migrations.
That conclusion doesn’t follow. Not because Flyway is broken, but because validate answers a different question than the one you’re asking.
When Flyway applies a migration, it computes a checksum of the file (CRC32 for SQL migrations) and stores it in the flyway_schema_history table alongside the version, description, and execution details.
validate recomputes checksums for the local migration files and compares them against what’s stored in the history table. It also checks that applied migrations still exist locally and flags pending or missing ones.
In other words, validate answers: “Have the migration files changed since they were applied, and is the file set consistent with the recorded history?”
It’s a file-integrity check. A good one — it catches the classic incident where someone edits an already-applied migration (even just fixing a typo in a comment) and every environment that applied the original version now disagrees with the repository.
Notice what’s absent from that description: the schema itself.
validate reads exactly one thing from your database — the history table. It never inspects your actual tables, columns, indexes, or constraints. So:
ALTER TABLE in production → files unchanged, history unchanged → validate passes
None of this is a bug. The tool is doing precisely what it’s scoped to do. The problem is the widespread belief that it’s scoped to do more.
A useful way to hold it in your head:
[...]The numbers here come from my own audit and design records as of 2026-08-15. Everything described as a redesign is a plan I have started building and have not proven yet.
Since June I have been building Looper, an experimental system for running long-lived autonomous loops against real work. In July I wrote about the control-theory core: every loop is a closed-loop controller, and the actuator must never reach its own sensor. This post is about the layer above that, and about a change in my mental model that came from measuring the system rather than admiring it. I spent most of the summer designing the organization of the agents, and what turned out to matter was the mechanism that keeps work alive after any particular agent disappears. In the rebuild that mechanism is a Postgres schema: a campaign row per open piece of work, leases with fencing tokens, a state machine enforced by triggers, append-only hash-chained events, and a stored-function API as the only write path. The agents became replaceable workers that Postgres wakes, hands a claim to, and outlives. If you read one section, read "Postgres as the coordination layer." The rest of this post is the evidence for that sentence and what I rebuilt because of it.
The idea, from June, was that I own the goals and everything below me is loops: controllers that sense, act, verify and adjust, each one a small lifecycle that is born from a goal, runs, learns and is retired. Many of them together would form what I called a civilization. I wrote a constitution for it, 18 articles on sensing honesty, independence, containment, lifecycle and the one seat a human never delegates. In August I added a section on warrants, eight more articles: signed, bounded, expiring grants of operational authority that flow down a chain from me to project heads to worker loops.
A good part of that has held up. Thinking in terms of an organization gave me:
Google launched AlloyDB in 2022. They claimed it is fully compatible with PostgreSQL. Can be up to 100 times faster for analytical queries than vanilla Postgres. Four years later, I haven't personally seen it gain significant traction. But it comes in discussions. When people ask me what AlloyDB actually is, I was able to pin point the features, but wasn't really sure what it delivers.
Over the past 12 months, I’ve evaluated AlloyDB. This article shares my key findings. I tried to keep it as objective as the topic allows, and where it isn't, the text says so. I won't pretend to be objective about the verdict. It depends less on one feature and more on where compatibility ends. And as it goes, "it depends" a lot on your workload and needs.
Let's start with the first claim. What does "fully compatible" mean? In this case it covers the wire protocol. Your psql connects, ORMs work, migration means changing connection string and you are done. What it does not cover is nearly everything you know about the Postgres storage internals and query executor. The 8KB pages that define storage layout for vanilla engine, are no longer the durable representation of your data. The WAL is no longer a recovery mechanism. It becomes the database. VACUUM is there, but runs inside storage layer you don't know and and schedule you can't control. And the tuning options differ from what you are be used to.
This creates the curious case. The compatibility claim is true, and the engineering behind AlloyDB backs it up. It's just narrower than the word "fully" migth suggest. AlloyDB is a different database behind the PostgreSQL protocol.
AlloyDB’s compatibility claim holds at the wire protocol, but the underlying engine diverges immediately at the storage layer.
Do you like building Postgres extensions? Of course you do! My "Let's Build a Postgres Extension" presentation garnered rave reviews at Postgres Conference 2026 in San Jose and PG Data 2026 in Chicago. But what if you didn't go? Sure the slides are available on both sites, but that's not quite the same, is it?Now that the dust has settled and my long series on Postgres 19 has finally reached its natural conclusion, let's get back to our regularly scheduled shenanigans. It's time to explore the exciting and daunting world of building an extension that does something fun: predicting query memory consumption, and potentially logging or blocking them based on results. Consider this a natural progression of my Introduction to Developing Postgres Extensions article.If you've ever wondered what the "right" setting for work_mem is, you're not alone. Each query node (join, sort, group) gets its own allocation, so large queries can command much more RAM than you might expect. Despite this, there's been no tool to give a rough estimate of how many allocations of a query might require. In the spirit of that missing tool, we'll be building something... semi-capable as a proof-of-concept. It's better to have something than nothing, after all. Unfortunately, almost nothing we need to predict lives in the manual. It lives in the source. Grab a machete, we're going into the untamed jungle.I hope you're ready, because by the time we're done, you'll be a Postgres extension artisan!
Please note different time and location this month at UpStart Collective at the U.S. Bancorp Tower (a.k.a. Big Pink). Please RSVP on MeetUp. Tuesday September 8, 2026 from 6:30pm to 8:30pm.
Coinciding with devopsdays Portland, OR, Sept 8-10, 2026.
In v18, Postgres got temporal primary keys, unique constraints, and foreign keys (with `NO ACTION`). Hopefully in v19 we’ll have `UPDATE/DELETE FOR PORTION OF`. So it’s a good time to start thinking about migrating your schema to a temporal structure. Some advantages include:
– Easier queries and joins to reconstruct historical data.
– A better way to do soft-deletes (preserving referential integrity).
– No bugs from foreign key references to since-updated data.
– A less ad hoc way of representing historical data.
This talk will explore how to migrate your schema to include application-time `daterange` and `tstzrange` columns. I’ll use an existing schema for a time-tracking and invoicing application (used by me for over 12 years), showing the pain points of the old structure, not sparing my pride at some bad decisions, and give an approach to bring it all into a nicer temporal structure.
We will also talk about some remaining pain points in using temporal tables, and suggestions to mitigate them.
If you are contemplating a move to temporal tables, this talk will give you an overview of the landscape.
Presented by Paul Jungwirth:
Paul is a freelance software developer in Portland, Oregon.
He has built applications with Postgres since 2010 and is the author of several extensions.
His Postgres contributions include work on GiST indexes, multiranges, and SQL:2011 application-time features.
In Part 6 of this special Postgres in Production deep dive series, Ryan Booz asks a question that determines how useful pg_stat_statements can be for you at all: do you have a high cardinality workload? This episode covers what that actually means, why ORMs, dynamic SQL, and AI-assisted development tools generate more unique queries than you might expect, a side by side demo of the same workload on Postgres 17 and Postgres 18, and the concrete checks that tell you whether pg_stat_statements is losing the data you need for query tuning.
Share this episode: Click here to share this episode on LinkedIn. Feel free to sign up for our newsletter and subscribe to our YouTube channel.
Transcript
Over the first five episodes we covered what pg_stat_statements is and the metrics it stores (Part 1), what makes a statement “unique” through normalization (Part 2), where the query texts live on disk (Part 3), how new metrics get stored and old ones get deallocated (Part 4), and the configuration settings that control all of it (Part 5).
Through all of that, I have probably mentioned the pg_stat_statements.max setting at least 100 times, because it’s so crucial to understanding how effective the data is that you have. This episode is about the workloads that consistently outrun that setting, and being able to identify whether you’re in that situation is really helpful to determining if pg_stat_statements can help you do the query tuning and optimization that you need it to.
The purpose of this blog post is to introduce pg_walviz, a new tool to visualize PostgreSQL WAL segment files.
pg_waldump is very useful to display a human-readable rendering of the WAL. However, sometimes we also want to see how records are physically stored in a segment: the WAL pages, record fragments, continuation records, alignment padding, block references, full-page images and raw bytes.
It is a read-only tool that displays a WAL segment in a local browser.
It presents the same WAL data at three levels:
Those views are synchronized. For example, selecting a page in the segment overview updates the list of WAL record fragments and the physical bytes. Selecting a record does the same for the other views. One can also go directly to a page number, record number, file offset or LSN.
As a picture is worth a thousand words, let’s have a look at it:
As you can see, the colors in the Physical Layout and Physical Bytes panels help to locate the record header, block headers, image headers, relation locator, block number, full-page images, main data and alignment padding.
Moving the mouse over a byte also displays its value, file offset, LSN, WAL page, record, fragment, XID and decoded component.
The current release is v0.1.0-beta.1 and can be found in this repository.
One can inspect a WAL segment with:
~/pg_walviz/bin/pg_walviz \
--pg-waldump /path/to/matching/postgres/bin/pg_waldump \
/archive/000000010000000000000042
The tool starts a local HTTP server, opens the browser and binds to 127.0.0.1 by default.
No running PostgreSQL server or data directory is required. A directory or several consecutive segment files can also
[...]My previous article talked about the checkpoint happiness hint: You probably should not change the checkpoint_timeout setting from its default of 5 minutes.
A good follow-up question was raised: can an HA replica can save you from downtime if you want to set a large checkpoint_timeout?
It’s true that Postgres allows promoting a replica without restarting, if there’s an unplanned primary restart and your primary is going to take an hour to come back online (after you increased checkpoint_timeout to 45 minutes). But this glosses over the fact that if the replica experiences a restart, then it will take an hour to start up too. Checkpoints on the primary directly translate into restartpoints on the replica (it’s the same WAL stream).
First case: everything is manually managed by a DBA and there’s little automation. Bugs in the tooling are a risk, but the biggest risk here is human error. As we often say in COE’s: people make mistakes. Hoping they won’t make a mistake is not a realistic plan for a reliable platform.
Second case: postgres is increasingly automated and we need to be careful that our automation doesn’t accidentally restart a replica while we’re promoting it.
Even with automation, common Postgres orchestration kits heavily rely on “the DBA knows how to configure it” (ie. you still can’t trust all of the defaults). One example: PG configuration changes require rolling restarts. Is the default behavior of common orchestration frameworks to continue a rolling restart even if the first node never comes back up? Are we back to the first case of relying on the DBAs to know the specific incantation of special commands they need to run, to ensure they never accidentally end up restarting both nodes? If the rolling restart can’t complete, will the DBA know how to address it without accidentally triggering a restart in any way?
And what if a query is triggering a postgres bug which causes a restart – like consuming enough memory to trigger OOM? This is r
[...]There is no shortage of PostgreSQL operators for Kubernetes. Projects such as CloudNativePG, the Zalando postgres-operator, Crunchy PGO, and StackGres have all helped shape the ecosystem.
Our answer is simple: multi-site PostgreSQL along with other capabilities.
The CYBERTEC PG Operator delivers the PostgreSQL lifecycle management expected from a Kubernetes operator, while placing a strong focus on operating PostgreSQL across multiple Kubernetes locations through Multi-site Clusters.
From cross-site replication and automated failover to validated deployment models, architecture guidance, and operational documentation, Multi-site Clusters were developed to address challenges that arise when PostgreSQL extends beyond a single Kubernetes environment.
The CYBERTEC PG Operator is open source, built on proven PostgreSQL technologies, and backed by CYBERTEC's PostgreSQL expertise.
We are excited to share the project with the community and look forward to your feedback and contributions.
Explore the source here: https://github.com/cybertec-postgresql/CYBERTEC-pg-operator
The post Introducing the CYBERTEC PG Operator appeared first on CYBERTEC PostgreSQL | Services & Support.
Formatting SQL tends to bring some of the same questions again and again: should we uppercase clause keywords? should we put the separating comma at the start of a line to ease refactoring? how to align the SQL clauses with one-another?
Over the years I have grown my own SQL style and didn’t find tooling that would implement it. Also, I’ve been asked here and there if there is a tool that would replicate The Art of PostgreSQL SQL indentation style… and now there is finally a good answer to that question!
sqlfmt is a gofmt-style formatter that implements my own favorite SQL indentation style. One opinionated style, no configuration knobs. Run it, commit the result, move on.
You have probably read a dozen tutorials on setting up PostgreSQL High Availability (HA). On paper, it looks simple: spin up a primary instance, spin up a standby, and let them replicate.
But what happens when you move to a multi-region architecture? What happens when your disaster recovery (DR) backup utility (like pgBackRest) strictly demands secure TLS (HTTPS) endpoints, but configuring native SSL on local storage/MinIO is an administrative nightmare? More importantly, when disaster strikes and you fail over to your DR region, how do you successfully fail back to your original primary region without running into timeline conflicts, operator deadlocks, or S3 archive poisoning?
This comprehensive guide takes you through the entire lifecycle—from the initial secure bootstrap to simulated regional failure, active failover, and the highly complex process of reversing roles (failback) using the Crunchy PostgreSQL Operator (PGO) on Kubernetes and MinIO as the secure WAL repository.
pgBackRest is extremely strict: it expects secure S3 endpoints over HTTPS. However, setting up native TLS directly on a local MinIO deployment is often over-engineered and tedious.
We solve this by deploying a lightweight NGINX Reverse Proxy in our minio namespace. This proxy terminates SSL/TLS on port 443 using a self-signed certificate and cleanly forwards plain HTTP traffic to MinIO on port 9000.
Generate a certificate valid for the proxy's in-cluster DNS name ( minio-secure.minio.svc.cluster.local):
# Generate the private key and self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key \
-out tls.crt \
-subj "/CN=minio-secure.minio.svc.cluster.local" \
-addext "subjectAltName = DNS:minio-secure.minio.svc.cluster.local"
# Save the TLS certificate inside the minio namespace
kubectl create secret tls minio-secure-tls \
--key tls.key \
--cert tls.crt \
-n minNumber of posts in the past two months
Number of posts in the past two months
Get in touch with the Planet PostgreSQL administrators at planet at postgresql.org.