Operator architecture
The Unified PostgreSQL Architecture: Scaling Without Complexity
Build a simpler PostgreSQL architecture with native queues, unlogged tables, GUC lock logging, connection pooling, HA patching, and exact NUMERIC types.
September 5, 2026·10 min read·OmniKit Editorial
SKIP LOCKED job queue
PostgreSQL documents SKIP LOCKED as a way to skip selected rows that cannot be locked immediately, instead of waiting. The docs state this is not suitable for general-purpose work, but can avoid lock contention when multiple consumers access a queue-like table. NOWAIT and SKIP LOCKED apply to row-level locks; the required ROW SHARE table-level lock is still taken normally.
- Pattern
- SELECT … FOR UPDATE SKIP LOCKED LIMIT n
- Behavior
- Skip currently locked rows; do not wait
- Not for
- General reads that need a consistent view of every row
- Date checked
- 5 September 2026
- Official source
- https://www.postgresql.org/docs/current/sql-select.html
LISTEN / NOTIFY
NOTIFY sends a channel event (optional payload) to sessions that previously executed LISTEN on that channel in the current database. Notifications from a transaction are delivered only after commit. Duplicate channel+payload pairs inside one transaction collapse to one event.
- LISTEN
- Register the session on a channel
- NOTIFY
- Deliver the event to listening sessions after commit
- Payload default max
- Shorter than 8000 bytes in the default configuration
- Date checked
- 5 September 2026
- Official source
- https://www.postgresql.org/docs/current/sql-notify.html
Lock-logging GUCs
log_lock_waits logs a session that waits longer than deadlock_timeout to acquire a lock. On PostgreSQL 18 the default is off. PostgreSQL 19 (beta) changes that default to on. log_lock_failures arrived in PostgreSQL 18 (default off) and logs who held the lock when row-level SELECT … NOWAIT fails. SKIP LOCKED never fails, so it does not produce that failure log. Do not lower deadlock_timeout only to get logs faster; the same timer fires the deadlock detector.
- log_lock_waits (≤18)
- Default off
- log_lock_waits (19 beta)
- Default on
- Wait threshold
- deadlock_timeout (default 1s)
- log_lock_failures
- New in 18; row-level NOWAIT misses, not LOCK TABLE NOWAIT
- Date checked
- 5 September 2026
- Official source (18 locks)
- https://www.postgresql.org/docs/current/runtime-config-locks.html
- 19 default / failure scope
- https://thebuild.com/blog/all-your-gucs-in-a-row-log_lock_waits-and-log_lock_failures/
session_replication_role
PostgreSQL documents session_replication_role as controlling replication-related triggers and rules for the current session (origin, replica, local). Because foreign keys are implemented as triggers, replica also disables foreign-key checks and can leave data inconsistent if misused. CHECK, NOT NULL, and UNIQUE still apply. PostgreSQL 18 documents ENFORCED | NOT ENFORCED on foreign keys and CHECK constraints as a per-constraint alternative.
- Default
- origin
- replica disables
- Default triggers/rules, including FK checks and ON DELETE CASCADE
- replica still enforces
- CHECK, NOT NULL, UNIQUE (no FK-style trigger)
- PG 18 alternative
- ALTER/CREATE … NOT ENFORCED on FK and CHECK
- Date checked
- 5 September 2026
- Official source
- https://www.postgresql.org/docs/current/runtime-config-client.html
- NOT ENFORCED
- https://www.postgresql.org/docs/current/sql-createtable.html
numeric vs floating point
PostgreSQL describes numeric as an exact, selectable-precision type and especially recommends it for monetary amounts and other quantities where exactness is required. real and double precision are inexact IEEE-754 types. If you require exact storage and calculations such as monetary amounts, the docs say to use numeric instead.
- numeric / decimal
- Exact; user-specified precision and scale
- real
- Inexact; ~6 decimal digits
- double precision
- Inexact; ~15 decimal digits
- Example (not a default)
- numeric(12, 2) — pick precision from the real domain
- Date checked
- 5 September 2026
- Official source
- https://www.postgresql.org/docs/current/datatype-numeric.html
PostgreSQL 19 status (September 2026)
PostgreSQL 18.6 and 19 Beta 3 were released on 13 August 2026. As of 5 September 2026, 19 is still in beta, not a production GA. The project advises against running beta in production. 19 enables log_lock_waits by default and ships other compatibility changes (JIT off by default, among others). Test against a representative workload before any upgrade.
- 19 Beta 3
- 13 August 2026
- Production
- Do not run beta in production
- log_lock_waits
- Default on in 19
- Date checked
- 5 September 2026
- Docs banner
- https://www.postgresql.org/docs/current/
- Release notes walkthrough
- https://tapoueh.org/blog/2026/09/getting-ready-for-postgresql-19/
Your application does not need Kafka, Redis, multiple microservices, and several database systems just because those technologies appear in large-scale engineering stacks. Adding infrastructure before your workload demands it creates more configuration, monitoring, security, and failure points. PostgreSQL already provides concurrency controls, notifications, ephemeral tables, transactions, replication, and strong data types that can cover a large part of an application's backend.
The better approach is simple: **start with PostgreSQL, measure the workload, and introduce another system only when PostgreSQL becomes the actual bottleneck.**
Use PostgreSQL as a Queue Before Adding Kafka
A background-job system does not automatically require Kafka, RabbitMQ, or another dedicated message broker. PostgreSQL can implement a database-backed queue using row-level locking and `SKIP LOCKED`.
FOR UPDATE SKIP LOCKED
The key pattern is documented in the PostgreSQL SELECT reference:
SELECT *
FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;`SKIP LOCKED` allows concurrent workers to skip rows that another transaction has already locked instead of waiting for those rows. PostgreSQL documents the feature specifically as a mechanism for skipping currently locked rows without waiting.
This works well for applications where jobs already belong to the same transactional data model as the rest of the application. A worker can claim a job, update its state, and process it without introducing another persistence system.
LISTEN and NOTIFY
You can also combine the queue with PostgreSQL's `LISTEN` and `NOTIFY`. A worker can listen for notifications and react when a producer signals that new work is available. PostgreSQL's NOTIFY documentation notes that `LISTEN` registers a session for notifications sent through a channel, while `NOTIFY` delivers those events to listening sessions.
When a dedicated broker is still the right next step
This does not mean PostgreSQL is a universal replacement for dedicated messaging infrastructure. If your workload requires very high message throughput, long retention, complex streaming semantics, or an event-stream architecture, evaluate dedicated tooling based on measured requirements.
Use Unlogged Tables for Data You Can Afford to Lose
Redis is useful, but not every cache, counter, or temporary state requires a separate Redis deployment.
Crash safety is the tradeoff
PostgreSQL supports unlogged tables for workloads where durability is not required. Unlike normal logged tables, changes to an unlogged table are not written to the Write-Ahead Log in the same way. The tradeoff is critical: unlogged data is not crash-safe and should not contain information that must survive a database failure.
That makes unlogged tables appropriate for selected ephemeral workloads, such as disposable staging data or certain rebuildable caches.
Do not treat this as a general-purpose replacement for durable application data. If losing the contents of a table after a crash would damage the application, an unlogged table is the wrong choice.
Know Where PostgreSQL Stops
Simplifying your architecture does not mean forcing PostgreSQL to do everything.
Keep blobs out of the relational core
PostgreSQL should not become your object-storage system. Large images, videos, backups, and other massive binary objects generally belong in dedicated object storage, while PostgreSQL stores the metadata or object identifier required to retrieve them. Compress and convert images before upload with the Image Optimizer; keep the database out of that path.
This separation keeps relational queries focused on relational data and avoids turning ordinary database operations into large-object management tasks.
The architectural principle is straightforward:
Use PostgreSQL for data that benefits from relational transactions, constraints, queries, and consistency. Use specialized infrastructure when the workload has fundamentally different storage requirements.
Tune GUCs to Find Lock Contention
When a PostgreSQL application becomes slow, adding hardware is not always the answer. Lock contention can cause requests to wait even when CPU and memory utilization appear acceptable.
PostgreSQL exposes configuration parameters known as Grand Unified Configuration (GUC) parameters. Two particularly useful settings for diagnosing lock problems are `log_lock_waits` and `log_lock_failures`.
| GUC | PostgreSQL 18 | PostgreSQL 19 (beta) | What you get |
|---|---|---|---|
| log_lock_waits | Default off | Default on | Log when a wait exceeds deadlock_timeout; then log when the lock is acquired |
| log_lock_failures | New; default off | Still a logging switch, not a wait | Who held the row lock when SELECT … NOWAIT failed |
| deadlock_timeout | Default 1s | Same timer still drives lock-wait logging | Do not cut this only to print logs sooner |
log_lock_waits
`log_lock_waits` causes PostgreSQL to log when a session waits longer than `deadlock_timeout` to acquire a lock. In PostgreSQL 18, this setting defaults to `off`. PostgreSQL 19 changes that default to `on`.
log_lock_failures
`log_lock_failures` was introduced in PostgreSQL 18. It produces detailed logging when lock acquisition fails, currently covering failures caused by `SELECT ... NOWAIT`.
These settings provide visibility into problems that otherwise appear to the application as unexplained latency.
Leave deadlock_timeout alone until you have a reason
Do not blindly reduce `deadlock_timeout` simply to generate logs faster. Configuration changes should follow observed workload behavior rather than arbitrary tuning recipes.
Build High Availability Around the Database
High availability is not only about PostgreSQL. Your operating system, database process, replication layer, connection routing, and application all participate in the availability design.
Rolling OS patching on a real HA cluster
With a properly configured PostgreSQL HA environment, operating-system maintenance can follow a rolling process. Replica nodes can be patched and restarted first while another node continues serving as the primary. After the replicas have caught up, the cluster can perform a controlled switchover, allowing the previous primary to be patched afterward.
The exact procedure depends on the HA tooling and deployment architecture. Patroni can manage PostgreSQL cluster leadership, while a connection-routing layer can direct applications toward the current primary.
The critical requirement is that applications must not hardcode a database node that may cease to be the primary.
Test failover before you need it
A failover process that exists only in documentation is not enough. Test promotions, switchovers, recovery, application reconnect behavior, and monitoring before you need them during an incident.
Control Connection Growth With Pooling
PostgreSQL uses a process-based server architecture, so connection management matters as applications scale. PostgreSQL's documentation states that the server uses one process per connection. Treat connection growth the same way you treat API concurrency: plan the budget with a rate-limit planner instead of opening a new backend for every short-lived client.
One process per connection
This becomes important for applications that create large numbers of short-lived connections, particularly serverless and highly concurrent workloads.
Transaction pooling
A connection pooler such as PgBouncer can reduce the number of PostgreSQL backend connections required by allowing many client connections to share a smaller pool of database connections.
Transaction pooling is particularly useful when application requests do not need to hold the same database connection across multiple transactions.
The goal is not to maximize `max_connections`. The goal is to keep the number of active PostgreSQL backends appropriate for the server's CPU, memory, workload, and query patterns.
Treat session_replication_role as a Dangerous Tool
Bulk imports and migrations sometimes tempt developers to use:
SET session_replication_role = replica;This is not a generic performance switch.
What replica mode actually disables
PostgreSQL documents `session_replication_role` as controlling the firing of replication-related triggers and rules for the current session. Because foreign keys are implemented using triggers, setting the role to `replica` also disables foreign-key checks. PostgreSQL explicitly warns that improper use can leave data inconsistent.
That means application logic depending on triggers can silently stop executing.
For example, if a trigger creates audit records, maintains derived data, or performs application-specific integrity work, disabling that trigger can leave the database in a state the application does not expect.
Use this setting only when you fully understand the data-loading process and its integrity implications. Do not use it as a shortcut for ordinary bulk inserts.
NOT ENFORCED constraints (PostgreSQL 18)
PostgreSQL 18 also introduced support for `NOT ENFORCED` constraints for foreign keys and `CHECK` constraints, providing a more explicit mechanism for cases where constraint enforcement needs to be controlled during schema operations. Spell that intent in schema and migration notes instead of hiding it in a session GUC.
Choose NUMERIC for Exact Financial Values
Data types are architectural decisions. Changing them later can require migrations, application changes, index work, and operational planning. Money columns belong in the same class of product decisions as SaaS unit economics: exact decimals, not approximations.
Exact numeric vs inexact float
For financial amounts and calculations where exact decimal results matter, PostgreSQL provides the `numeric` type. PostgreSQL describes `numeric` as an exact, selectable-precision type and specifically recommends it for monetary amounts and quantities where exactness is required.
Floating-point types such as `real` and `double precision` are inexact representations. They can be appropriate for scientific, statistical, or engineering workloads where their characteristics are acceptable, but they are not automatically the right choice for monetary values.
A practical schema decision is therefore:
CREATE TABLE invoices (
id bigint PRIMARY KEY,
total numeric(12, 2) NOT NULL
);The appropriate precision and scale depend on the application's actual requirements. Do not copy a precision value blindly from an example.
Keep PostgreSQL Current Without Chasing Every Release
PostgreSQL continues to evolve, and version upgrades can introduce meaningful improvements in performance, security, observability, and developer capabilities. The same rule applies as with a runtime upgrade: ship the version you have tested, not the version that just appeared.
PostgreSQL 19 is still beta
As of September 2026, PostgreSQL 19 is in its beta cycle rather than being a release that should automatically be deployed to production. The PostgreSQL project explicitly advises against running beta versions in production and recommends testing them against representative workloads.
PostgreSQL 19 includes changes across performance, monitoring, security, replication, and SQL capabilities. It also enables `log_lock_waits` by default.
For production systems, the correct strategy is not to upgrade simply because a new major version exists. Evaluate compatibility, extensions, drivers, migration procedures, rollback options, and application behavior first.
A Simpler PostgreSQL Architecture Is Usually Easier to Operate
The strongest PostgreSQL architecture is not the one with the most infrastructure. It is the one that meets the workload's requirements without creating unnecessary operational complexity.
Use PostgreSQL's native locking features for appropriate job queues. Use `LISTEN` and `NOTIFY` when database notifications fit the workload. Use unlogged tables only for data that can safely disappear. Diagnose contention through GUCs instead of guessing. Use connection pooling when connection pressure becomes a problem. Design HA around tested failover and routing. Treat `session_replication_role` as a specialized administrative mechanism, not an everyday optimization. Choose exact numeric types when the business requires exact decimal arithmetic.
Then measure. Put the numbers on a page assistants can cite — GEO Brand Auditor scores whether those facts actually get mentioned — and keep a llms.txt hint pointed at the live pages, not at this file.
If PostgreSQL becomes the bottleneck, identify the specific bottleneck before replacing it. The right architecture is driven by workload characteristics, not by copying the infrastructure of companies operating at a completely different scale. If you want a second pair of eyes on that decision, contact OmniKit.
Frequently asked questions
Can PostgreSQL replace Kafka for a job queue?
For jobs that already live in the same transactional data model, yes: FOR UPDATE SKIP LOCKED lets workers skip locked rows instead of waiting. Combine with LISTEN/NOTIFY when you want a wake-up signal. Use a dedicated broker when you need very high throughput, long retention, or streaming semantics that you have actually measured.
When should you use an unlogged table instead of Redis?
When the data is disposable: staging, rebuildable caches, counters you can lose on crash. Unlogged tables skip normal WAL durability. If losing the table after a crash damages the application, keep a logged table or a purpose-built cache.
What is the difference between log_lock_waits and log_lock_failures?
log_lock_waits records waits longer than deadlock_timeout (default off through PostgreSQL 18; default on in 19 beta). log_lock_failures (new in 18) records who held the lock when a row-level SELECT … NOWAIT failed. SKIP LOCKED does not fail, so it does not hit that log.
Is SET session_replication_role = replica a bulk-load speed trick?
No. It is a replication-apply setting. replica disables default triggers and therefore foreign-key checks. Audit triggers also go quiet. CHECK and NOT NULL still fire. Prefer an explicit NOT ENFORCED constraint (PostgreSQL 18) when you need to control one constraint during schema work.
Should money columns use double precision?
No, not when exact decimal results matter. PostgreSQL recommends numeric for monetary amounts. real and double precision are inexact. Pick precision and scale from the domain; do not copy numeric(12, 2) from an example.
Should you run PostgreSQL 19 in production in September 2026?
No. PostgreSQL 19 Beta 3 shipped 13 August 2026. The project tells you not to run beta in production. Test it against a representative workload, then upgrade after GA and a compatibility review.
How do you patch the OS on a PostgreSQL HA cluster without a full outage?
Patch and rejoin replicas first, wait until replication is caught up, switch the primary to an already-patched node, then patch the old primary. Applications must connect through routing that follows the current primary, not a hardcoded node. A single-node database still needs a maintenance window.