Realtime (LISTEN / NOTIFY)
$listen and $notify send events between processes through PostgreSQL LISTEN / NOTIFY, with no broker to run. Use them for cache invalidation, live dashboards and worker fan-out.
Subscribe and publish#
const sub = await db.$listen('order_created', (payload) => {
const order = JSON.parse(payload);
console.log('new order:', order.id);
});
await db.$notify('order_created', JSON.stringify({ id: 1 }));
await db.$notify('cache_invalidated'); // payload defaults to ''
await sub.unsubscribe();$listen resolves once the subscription is live. It holds one pooled connection until you call unsubscribe(). In a long-lived worker, db.disconnect() releases every subscription on shutdown:
await db.$listen('cache_invalidated', (key) => cache.delete(key));
process.on('SIGTERM', async () => {
await db.disconnect(); // unsubscribes and cancels any pending reconnect
});$notify is one round trip: SELECT pg_notify($1, $2) with both values bound. The payload is a string, so serialize structured data.
Lost connections#
A restart, failover or compute suspend eventually closes the subscription's connection. $listen then reconnects with exponential backoff and runs LISTEN again:
const sub = await db.$listen('order_created', onOrder, {
onReconnect: () => refreshOrders(), // resync: anything sent while down is gone
onError: (err) => log.warn(err), // each loss, as a ConnectionError (E004)
});| Option | Default | What it does |
|---|---|---|
reconnect | true | false ends the subscription at the first loss. { initialDelayMs, maxDelayMs } tunes the backoff (default 100 ms doubling to 30 s) |
onError | a line through the client's logger | Called on each loss and each failed reconnect |
onReconnect | none | Called once the subscription is live again |
NOTIFY is not durable: a notification sent while the subscription is down is never delivered. Treat onReconnect as the signal to re-read the state the channel tracks.
Limits#
- Channel names must be plain identifiers (letters, digits, underscores, not starting with a digit, at most 63 characters). Anything else,
'order created'or'app.events'for instance, throwsValidationError(E003) before a connection is used. $listenneeds a persistent connection. On an HTTP driver (Neon HTTP, Vercel Postgres) it throwsConnectionErrorinstead of hanging. Run it in a long-lived process;$notifyworks everywhere.- PostgreSQL only. Other engines throw
UnsupportedFeatureError(E017).
See also#
- Serverless & Edge: which APIs work on HTTP drivers.
- Typed errors:
ValidationError,ConnectionErrorand the rest.