A realtime database is one that pushes changes to connected clients the moment they happen, instead of waiting for the client to ask again. The term is used for two different products: hosted JSON stores with client SDKs that keep every connected device in sync, and analytical databases that ingest event streams and answer queries over the last few seconds. Most people searching for it want the first. The first can be bought as a service, with a data model and a vendor attached, or built on the relational database you already run, with LISTEN and NOTIFY, change data capture and a websocket in front.
The search results are the hosted product’s documentation on one side and analytics vendors redefining the phrase on the other, with a couple of open-source projects in between. None of them says the thing a developer with an existing Postgres wants to hear: you probably do not need a new database to get realtime behaviour. You need a push channel and a source of change events, and Postgres has had both for a long time.
Table of contents
- Two different things called realtime
- Push, not poll
- What a hosted realtime database gives you, and takes
- Getting realtime from Postgres
- The honest limits of LISTEN and NOTIFY
- Do you need it at all
- How this fits the rest of the stack
- FAQ
Two different things called realtime
The first meaning is synchronisation. A client subscribes to a piece of data, and when anyone changes it, every subscriber receives the new value within milliseconds. The hosted product that made the term famous stores data as one large JSON tree, ships SDKs for web and mobile, works offline and reconciles on reconnect, and enforces access with a rules language evaluated on every read and write. It is a genuinely good fit for chat, presence, collaborative editing and live dashboards on mobile.
The second meaning is analytics. Vendors selling columnar or streaming engines use realtime database to mean a store that ingests millions of events a minute and answers aggregate queries over them with sub-second latency. That is a different problem with a different buyer, and if you have it, you know. The rest of this post is about the first meaning, because that is what the question usually is.
Push, not poll
The behaviour that makes a database realtime is not in the storage engine. It is in the delivery: the server pushes changes over a connection that stays open, rather than the client re-fetching on a timer. Polling every few seconds is not real-time; it is a nervous refresh button wearing a fake moustache, and it costs a query per client per interval whether or not anything changed.
The two push channels a browser understands are websockets and server-sent events. Websockets are bidirectional and suit chat and collaboration; SSE is one-way, simpler, and enough for feeds and dashboards. Either sits between the clients and the database, which means realtime is a small service you run, not a property you buy. The WebSocket with nginx post covers the proxy configuration that trips most first deployments.
What a hosted realtime database gives you, and takes
The hosted products deliver the whole stack: storage, subscriptions, client libraries, offline caching, authentication and the rules layer. For a mobile app built by two people, that is weeks saved, and the pricing is generous until it is not.
- The data model is a JSON tree or a document collection. No joins, no transactions across the tree, and denormalisation is the recommended pattern, so the same fact lives in several places and you keep them in step by hand.
- The rules language is the only access control. It is expressive, and it is also a second codebase that lives outside your tests.
- The client talks to the database directly. There is no server between them to add logic, rate-limit or audit, unless you add one, at which point the simplicity is gone.
- Leaving means exporting a tree into tables. Teams that outgrow the model discover that the model is the product.
The Firebase vs Supabase post compares the tree-shaped product with a relational one that adds realtime on top, which is the same trade-off this post makes with plain Postgres.
Getting realtime from Postgres
Postgres has a built-in publish-subscribe mechanism. NOTIFY sends a message on a named channel; any connection that ran LISTEN on that channel receives it. Put the NOTIFY in a trigger, and every insert or update on a table becomes an event with no application code involved.
CREATE OR REPLACE FUNCTION notify_order_change() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('orders', json_build_object(
'op', TG_OP, 'id', NEW.id, 'status', NEW.status
)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_notify
AFTER INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_change();
A small Node service holds one connection that listens, and fans each message out to the websocket or SSE clients that care about that channel:
import pg from 'pg';
import { WebSocketServer } from 'ws';
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query('LISTEN orders');
const wss = new WebSocketServer({ port: process.env.PORT || 8080 });
client.on('notification', (msg) => {
for (const ws of wss.clients) ws.send(msg.payload);
});
That is a realtime database in about thirty lines, on the database that already holds the orders, with joins, transactions and the access control you already have. For higher volumes or guaranteed delivery, logical replication is the next step: the database streams every committed change from its write-ahead log to a consumer, which is what the change-data-capture tools and the relational realtime products are built on.
The honest limits of LISTEN and NOTIFY
It is not free of trade-offs, and the ones that bite are worth knowing before the design depends on it.
- Payloads are capped at 8000 bytes. Send the id and the fields that changed, and let the client fetch the rest if it needs it.
- Notifications are not stored. A listener that is disconnected when the event fires never sees it. If a client must not miss anything, it reconciles on reconnect by querying what changed since a timestamp or sequence.
- Each listener is a database connection. Do not have every web client listen directly; one service listens and fans out, or the connection limit is the first thing you hit. The database connection limits docs explain why that ceiling is where it is.
- Notifications are delivered on commit, in order, per connection. Under heavy write load the queue can back up and the listener falls behind; the fix is to batch or move to logical replication.
Do you need it at all
The question to ask is which events a user must see without acting. An order status on a screen someone is staring at, yes. A chat message, yes. A weekly report, no. Most applications have two or three genuinely realtime surfaces and a great many that are fine being fresh on the next navigation. Build the push channel for the two or three, on the data you already have, and leave the rest alone.
On RunxBuild that design is a managed Postgres with the trigger in it and a Node service that listens and serves the websocket, connected over a private network and deployed from the same repository. The service runs on the Basic plan at $6 a month (0.5 vCPU, 624MB), the database has scheduled backups and its own connection limit, and both scale independently, which matters because the websocket service grows with connected clients and the database grows with data. The Node services docs cover the deployment; the PostgreSQL connection string post covers the one environment variable the listener needs.
How this fits the rest of the stack
Realtime is a delivery property, not a storage engine. The hosted products bundle it with a data model and a vendor; Postgres provides it with a trigger, a listening service and a websocket, on tables you can still join. Decide which surfaces genuinely need push, build the channel for those, and reconcile on reconnect so a dropped connection is an inconvenience rather than data loss. The RunxBuild hosting calculator prices the database and the small listening service together, so the cost of realtime on your own data is a line item rather than a platform migration. Ship the trigger, keep the logs close, and stop polling.
Useful related references:
- WebSockets Behind Nginx: A Config That Survives Production
- Firebase vs Supabase: Document Store or Postgres, and What That Costs You Later
- PostgreSQL Connection String: libpq URI, Keyword/Value, and Code Examples
- Database connection limits on RunxBuild
FAQ
What is a realtime database?
A database that pushes changes to connected clients as they happen, rather than waiting for the client to query again. The best-known hosted version stores data as a JSON tree and syncs it to web and mobile SDKs. The same behaviour can be built on a relational database with change notifications and a websocket or server-sent-events service in front.
Is PostgreSQL a realtime database?
Not out of the box, but it has the pieces. LISTEN and NOTIFY provide publish-subscribe on the server, triggers turn table changes into notifications, and logical replication streams every committed change for higher volumes. A small service that listens and fans out over websockets gives Postgres the same realtime behaviour with joins and transactions intact.
WebSockets or server-sent events for realtime updates?
Server-sent events if the data only flows from server to client, such as dashboards and feeds: they are simpler, work over plain HTTP and reconnect automatically. WebSockets if the client also sends messages over the same connection, such as chat or collaborative editing. Both need a proxy that keeps long-lived connections open.
What is the difference between Firebase Realtime Database and Firestore?
Both are hosted realtime databases from the same vendor. The original stores one large JSON tree and is optimised for low-latency sync of small values; the newer one stores documents in collections with richer queries and better scaling. New projects usually start on the document store; the tree version remains for presence and very high-frequency small updates.
How many clients can Postgres LISTEN and NOTIFY support?
Each listener is a database connection, so the limit is the connection ceiling, typically low hundreds on a small instance. The pattern is one listening service, not one connection per browser: that service holds a single connection and fans notifications out to thousands of websocket clients. Beyond that, logical replication replaces NOTIFY as the event source.