There’s a moment every backend developer hits eventually: you’re polling your database every couple of seconds to check for new data, and it just feels wrong. It works, sure, but it’s wasteful, it adds latency, and it makes your database do a bunch of unnecessary work just to tell you “nope, nothing new” over and over again.
PostgreSQL has a built-in answer to this, and it’s the LISTEN command. I’m going to walk you through exactly how it works, how to pair it with NOTIFY, and how to actually use it in real applications without running into the gotchas that trip up a lot of people the first time they try it.
What Does LISTEN Do?
LISTEN registers the current database session as a subscriber to a named notification channel. Once you run it, that session will receive any messages sent to that channel via the NOTIFY command, for as long as the session stays connected.
It’s the receiving half of PostgreSQL’s built-in publish/subscribe system. NOTIFY sends messages, LISTEN receives them. Neither one does anything useful without the other.
What makes this special is that it’s asynchronous and near-instant. You don’t need to run a SELECT query in a loop to check for updates — PostgreSQL pushes the notification to you the moment it happens (well, technically the moment the notifying transaction commits, but more on that shortly).
Basic Syntax
The syntax couldn’t be simpler:
LISTEN channel_name;
channel_name is an identifier — the same rules apply as with table or column names. It can’t start with a digit, and if you want to use characters outside the standard identifier rules (like spaces or mixed case that needs to be preserved), you’d wrap it in double quotes:
LISTEN "My Channel";
But honestly, I’d recommend sticking to simple lowercase names with underscores, like order_updates or job_queue_new, just to keep things predictable across your codebase.
To stop listening, there’s a matching command:
UNLISTEN channel_name;
Or, to stop listening to everything at once:
UNLISTEN *;
How LISTEN Actually Works Under the Hood
When you issue LISTEN, PostgreSQL records that your current backend process (your session) is subscribed to that channel. This subscription lives only as long as your session is connected. Close the connection, and you’re automatically unsubscribed — there’s no persistence across reconnects. If your app restarts or the connection drops, you need to issue LISTEN again once reconnected.
When another session runs NOTIFY channel_name (or pg_notify()), and that transaction commits successfully, PostgreSQL delivers the notification to every currently listening session on that channel, including — importantly — the session that sent it, if it happens to also be listening on the same channel.
The delivery itself happens via PostgreSQL’s underlying connection protocol. Your client library needs to actively check for and surface these asynchronous messages; they don’t just magically appear as query results. This is the part that confuses people most often, so let’s spend some real time on it.
Trying LISTEN in psql
The quickest way to see this work is with two psql windows open side by side.
Terminal 1:
LISTEN chat_messages;
You won’t see any output yet — you’re just now subscribed.
Terminal 2:
NOTIFY chat_messages, 'Hello from another session!';
Back in Terminal 1, run any trivial query (like SELECT 1;) or just wait — psql checks for notifications between commands — and you’ll see:
Asynchronous notification "chat_messages" with payload "Hello from another session!" received from server process with PID 18820.
That’s PostgreSQL’s asynchronous messaging working exactly as designed.
Using LISTEN From Application Code
This is where things get more interesting, because every language and driver handles the mechanics of receiving notifications a bit differently. Let’s go through a few common ones.
Node.js with node-postgres (pg)
const { Client } = require('pg');
const client = new Client({
connectionString: 'postgres://user:password@localhost/mydb'
});
client.connect();
client.query('LISTEN order_updates');
client.on('notification', (msg) => {
console.log('Channel:', msg.channel);
console.log('Payload:', msg.payload);
// Handle the notification here
});
The key detail: you need to keep this client connection open and dedicated. Don’t grab it from a general-purpose connection pool that recycles connections between unrelated queries, because your LISTEN subscription will vanish the moment that connection is returned to the pool and reused (or worse, closed).
Python with psycopg2
import psycopg2
import select
conn = psycopg2.connect("dbname=mydb user=myuser")
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute("LISTEN order_updates;")
print("Waiting for notifications...")
while True:
if select.select([conn], [], [], 5) == ([], [], []):
continue
else:
conn.poll()
while conn.notifies:
notify = conn.notifies.pop(0)
print(f"Got NOTIFY: {notify.channel} -> {notify.payload}")
Notice the select() call — this is how Python efficiently waits for the socket to have data available, rather than busy-looping and burning CPU.
Python with asyncpg (async)
import asyncio
import asyncpg
async def handle_notification(connection, pid, channel, payload):
print(f"Received: {channel} -> {payload}")
async def main():
conn = await asyncpg.connect("postgresql://user:password@localhost/mydb")
await conn.add_listener('order_updates', handle_notification)
await asyncio.sleep(3600) # keep the connection alive to listen
asyncio.run(main())
asyncpg handles a lot of the plumbing for you, which is one reason it’s a popular choice for real-time PostgreSQL-driven applications in Python.
A Real Example: LISTEN for a Job Queue Worker
Let’s build something more realistic. Suppose you have a jobs table, and worker processes should wake up immediately when new jobs arrive rather than polling every few seconds.
Setup:
CREATE TABLE jobs (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE OR REPLACE FUNCTION notify_job_inserted()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('job_inserted', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_notify_job_inserted
AFTER INSERT ON jobs
FOR EACH ROW
EXECUTE FUNCTION notify_job_inserted();
Worker (conceptual pseudocode using any driver):
LISTEN job_inserted;
loop forever:
wait for notification (with a timeout, e.g. 30 seconds, as a safety net)
if notification received OR timeout elapsed:
SELECT * FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
process the job if one was found
Notice that even though LISTEN wakes the worker up instantly, the worker still queries the table for the actual work — it doesn’t trust the notification payload as the sole source of truth. This pattern (notify-to-wake, then query-to-confirm) is by far the most robust way to use LISTEN for queue-like workloads, because it protects you from missed notifications, duplicate deliveries, or notifications lost during a brief disconnect.
Common Use Cases
- Real-time UI updates — Backend listens for database changes and pushes them to connected clients over WebSockets.
- Distributed cache invalidation — Multiple app servers listen on a shared channel so they all evict stale cache entries at once.
- Background job wake-ups — As shown above, listening lets workers avoid constant polling while still remaining safe against missed events.
- Multi-tenant event routing — Applications with per-tenant channels (e.g.,
tenant_42_updates) can isolate notification streams cleanly. - Leader election or coordination signals — Lightweight coordination between service instances without needing a separate coordination service.
Troubleshooting LISTEN Issues
“I ran LISTEN but I’m not getting anything.” Ninety percent of the time, this is a connection pooling issue. Your LISTEN subscription is tied to one specific database connection. If your application framework uses a connection pool (which most ORMs do by default), the connection you used to run LISTEN might get returned to the pool and handed to a completely different part of your app for an unrelated query, silently breaking your subscription. Always open a separate, dedicated, long-lived connection specifically for listening.
“My connection keeps dropping and I stop receiving notifications.” Networks aren’t perfect, and database connections can drop due to timeouts, restarts, or load balancer behavior. Build reconnection logic that automatically re-issues LISTEN after any reconnect. Don’t assume your subscription persists across a dropped connection — it doesn’t.
“I’m missing notifications that happened while my app was restarting.” This is expected behavior, not a bug. LISTEN/NOTIFY has no memory or replay mechanism. If you need guaranteed delivery even across downtime, you need a durable queue or, at the very least, a “catch-up” query on startup that checks for anything you might have missed.
“Too many idle connections just for listening.” If you have many independent features all needing to listen for different things, consider consolidating into fewer long-lived connections that each subscribe to multiple channels, rather than opening a separate connection per feature. LISTEN supports subscribing to more than one channel per session — just call it multiple times.
Best Practices
- Use one dedicated connection per listening process, separate from your regular query connection pool.
- Always build in reconnection logic that re-issues
LISTENcommands after a dropped connection. - Don’t treat notifications as guaranteed delivery. Use them as a wake-up trigger, then confirm state with an actual query, especially for anything important.
- Set a reasonable timeout when waiting for notifications so your process periodically checks state anyway, as a safety net against missed events.
- Keep channel names organized and documented, especially as your application grows and more features start listening on different channels.
- Monitor your listening connections. Since these are typically long-lived, idle connections, keep an eye on connection counts and make sure you’re not accidentally leaking listener connections that never get cleaned up.
Wrapping Up
LISTEN is deceptively simple on the surface — one line of SQL — but using it well in a real application means understanding its session-bound nature, its lack of persistence, and how your specific client library surfaces asynchronous notifications. Once you’ve got that down, it becomes an incredibly cheap way to make your application feel responsive and real-time, without bolting on an entirely separate messaging system. Combined with NOTIFY, it’s one of PostgreSQL’s most underrated features, and it’s sitting right there in every PostgreSQL install, ready to use for free.
