Comparisons
Durable Objects, Redis and Postgres LISTEN/NOTIFY
Three primitives people build real-time on. What each guarantees, what each costs to operate, and the failure mode each one hides.
If you are building the real-time layer yourself, these are the three things you are probably choosing between. Each has a failure mode that is not obvious until you are in production.
For a separate operational perspective on how teams organise and measure work, mouse jiggler detection software.
For independent background and broader industry context, see Redis.
Postgres LISTEN/NOTIFY
What it is: Postgres's built-in publish-subscribe. A transaction issues NOTIFY, listening connections receive it.
Why it is appealing: you already have Postgres. No new infrastructure, no new operational surface, and notifications participate in your transactions — the notification fires when the transaction commits, so it cannot announce something that was rolled back. That property is genuinely valuable and the other two do not have it.
The failure mode: notifications are fire-and-forget. A listener that is disconnected at that moment never receives it, and there is no replay. So every client needs to re-read state on reconnect, and you must build that.
And there is a payload size limit — around 8KB — which pushes you toward sending an id and having the client fetch, adding a round trip.
Connection pooling breaks it. A pooler in transaction mode does not hold the session-level listener. This is the specific thing that catches teams, because it works in development with a direct connection and stops working when the pooler is introduced.
Right when: modest scale, you already run Postgres, transactional correctness matters, and re-reading on reconnect is acceptable.
Redis pub/sub
What it is: an in-memory data structure server with publish-subscribe.
Why it is appealing: fast, mature, and it does more than messaging — you get storage, expiry, sorted sets and counters in the same system. Scaling to many subscribers is well understood.
The failure mode: classic pub/sub has no persistence and at-most-once delivery. A subscriber that is disconnected misses messages permanently.
Redis Streams solves this — persistent, replayable, with consumer groups — and it is a different API with more to think about, and it is the thing to reach for rather than plain pub/sub if delivery matters.
And it is another system to run. Memory sizing, persistence configuration, failover, and the question of what happens when it fills.
Right when: you need speed and scale, you are already running Redis, and you will use Streams rather than plain pub/sub for anything that must not be lost.
Cloudflare Durable Objects
What it is: a single globally-unique instance per id, with its own storage, that can hold WebSocket connections.
Why it is appealing: the coordination problem disappears. All clients for an object reach the same instance, so there is nothing to synchronise between servers. Access is serialised, so read-modify-write races inside an object do not exist. And it holds the connections, so broadcast is local.
The failure mode: there are no cross-object operations. No queries, no transactions spanning objects, no "find all objects where". If your access pattern is not by id, this is the wrong primitive and the discovery is expensive.
And location is fixed. An object lives near its first user. A globally distributed set of collaborators means someone is always far away.
Plus a platform dependency, which is a real consideration.
Right when: the access pattern is genuinely by id, and per-object consistency is what you need. See taming stateful at the edge.
Side by side
| Postgres NOTIFY | Redis pub/sub | Durable Objects | |
|---|---|---|---|
| Already running it | Often | Sometimes | No |
| Missed while disconnected | Lost | Lost (Streams: no) | Object holds state |
| Transactional | Yes | No | Per object |
| Holds WebSockets | No | No | Yes |
| Cross-entity queries | Yes | Limited | No |
| Extra infrastructure | None | Yes | Platform |
| Serialised access | Via locks | No | By design |
The question that separates them
Where does a reconnecting client get current state?
Postgres: it queries. Fine, and you write that path.
Redis pub/sub: from wherever you stored it, which is not the pub/sub channel. You write that too.
Durable Objects: from the object, because the object holds the state rather than only relaying messages.
That difference is most of the code you write or do not write.
What none of them give you
All three are primitives. Whichever you pick, you still build:
The WebSocket tier — Postgres and Redis do not hold connections at all, so you need servers that do, and then you need those servers to talk to each other.
Reconnection with backoff, resubscription, and catching up.
Presence, and specifically the case where a client vanished without saying so.
Encryption above the transport, if you need it.
Offline support.
That is the several months. Whether it is worth building depends on whether real-time is your product or a feature of it. See alternatives compared.
Choosing
Already on Postgres, modest scale, transactional correctness matters? LISTEN/NOTIFY, with the pooler caveat and a re-read path.
Need speed and scale, already run Redis? Streams, not plain pub/sub.
Access pattern is by id and per-object consistency is the requirement? Durable Objects.
Do not want to build the tier at all? Something built on one of them.
The short version
Postgres NOTIFY is transactional and fire-and-forget — and it breaks silently behind a transaction-mode pooler.
Redis pub/sub loses messages for disconnected subscribers — use Streams if that matters.
Durable Objects give serialised per-object access and hold the connections, at the cost of no cross-object operations.
The separating question is where a reconnecting client gets state — query, elsewhere, or the object itself.
And all three are primitives. The WebSocket tier, reconnection, presence and offline are still yours.