Hacktakes · Edition 12
Hacktakes · Edition 12 · July 25, 2026

what does a postgres NOTIFY look like on the wire?

Postgres handles asynchronous pub/sub by natively multiplexing unprompted messages over the exact same TCP stream used for synchronous queries.

By Poppy Lin

Sparked by Postgres LISTEN/NOTIFY actually scales · discussion

It’s a great setup—when you aren't running the water, the city just uses the pipe to asynchronously push your mail.
It’s a great setup—when you aren't running the water, the city just uses the pipe to asynchronously push your mail.

I was reading a really interesting Hacker News thread yesterday about a blog post on Postgres LISTEN/NOTIFY scalability. The thread was debating high-throughput queuing limits, but honestly, the whole conversation just made me realize something much more basic. I realized I have ABSOLUTELY NO MENTAL MODEL for how a database pushes data to a client! Whenever I hear the word "pub/sub", I immediately imagine deploying Kafka, getting confused by partitions, and crying. I always assumed databases just sat there passively waiting for you to send a query.

So, I wanted to figure out what an asynchronous database push actually looks like on the wire. We are going to completely ignore the complex scalability debate today because what I really want to know is the raw byte mechanics of how a push notification physically arrives at an application.

My naive assumption was that Postgres had to open a secret secondary port to push data to listening clients. I tend to think of databases like standard HTTP web servers: the client speaks, the server responds, and then everyone hangs up. The idea of the server waking up ten minutes later to shove unprompted data down a dormant pipe feels like a violation of the rules. (I figured there was no way it could just randomly interrupt an active connection that was designed for synchronous requests).

To see what is actually happening physically, imagine a scenario where you set up a local Postgres instance and disable TLS by passing sslmode=disable so the raw network traffic is readable in plaintext. You open one terminal and run a simple LISTEN my_channel; command, leaving it idle. Then, you open a second terminal and fire off NOTIFY my_channel, 'hello!'; to trigger the push event.

And if you capture the traffic on port 5432 during that exact second with a tool like tcpdump, you get a completely unprompted transmission from the server to the idle client. Here is the literal hex payload that comes across the wire:

14:23:10.123456 IP 127.0.0.1.5432 > 127.0.0.1.54321: Flags [P.], length 27
    0x0000:  4100 0000 1b00 000f a46d 795f 6368 616e  A........my_chan
    0x0010:  6e65 6c00 6865 6c6c 6f21 00              nel.hello!.
14:23:10.123500 IP 127.0.0.1.54321 > 127.0.0.1.5432: Flags [.], ack 27

What is going on here? If you cross-reference this hex dump with the Postgres Wire Protocol v3 documentation, the translation is surprisingly human-readable. The protocol has a specific format called NotificationResponse, and it maps perfectly to these raw bytes.

But the most important part of this entire trace is that very first byte at the top left.

The first byte is 0x41, which is the literal ASCII letter 'A'. That single character is the flag the Postgres server uses to loudly announce that it is sending an asynchronous notification rather than responding to a normal query. Next, the trace shows 0000 001b, which is just the total length of the message in bytes. Then we see 0000 0fa4, which represents the process ID of the notifying backend. Finally, you can look at the raw ASCII translation on the right side of the packet and clearly read my_channel followed by hello!, both separated by a null byte.

Okay, so the server literally just spells out the channel and the message in plain text.

This totally changes how I understand database clients. When you run a normal SELECT query, the server usually responds with a stream of bytes starting with 'T' (RowDescription) or 'D' (DataRow). Your database driver sits there reading those bytes and formatting them into a nice array for your application. When you issue a LISTEN command, the driver simply keeps that exact same TCP connection open and enters a waiting state. Because it is constantly reading from the socket, if a packet suddenly arrives starting with 'A' instead of 'D', the driver catches it. It reads the 'A', realizes it is looking at a pub/sub event instead of a database row, and triggers your asynchronous callback.

Instead of relying on a magical background queuing infrastructure, Postgres simply multiplexes these unprompted messages natively over the exact same, completely ordinary TCP stream used for synchronous queries. It is wildly elegant. You don't need a massive distributed architecture to send events; you just need a persistent network socket and an agreed-upon alphabet.

I am seriously so amazed by this! I spent years assuming pub/sub meant you had to stand up a terrifying distributed message broker, and it turns out Postgres is just casually sneaking an ASCII 'A' down the exact same TCP pipe you use to query your users table. Code that works is so incredibly exciting :)

← Back to Edition 12