PostgreSQL LISTEN/NOTIFY kan live-dashboards en meldingen aandrijven met minimale setup. Lees waar het past, wat de beperkingen zijn en wanneer je een berichtbroker moet inzetten.

"Live-updates" in een productgebruikersinterface betekent meestal dat het scherm kort na een gebeurtenis verandert, zonder dat de gebruiker ververst. Een getal op een dashboard loopt op, een rood badge verschijnt in een inbox, een beheerder ziet een nieuwe bestelling of er verschijnt een toast met "Build finished" of "Payment failed". Het gaat om timing: het voelt direct, ook al duurt het soms een seconde of twee.
Veel teams beginnen met polling: de browser vraagt de server elke paar seconden "is er iets nieuw?". Polling werkt, maar heeft twee veelvoorkomende nadelen.
Ten eerste voelt het traag omdat gebruikers pas bij de volgende poll veranderingen zien.
Ten tweede kan het duur worden omdat je herhaaldelijk controleert, zelfs als er niets veranderd is. Vermenigvuldig dat met duizenden gebruikers en het wordt ruis.
PostgreSQL LISTEN/NOTIFY bestaat voor een eenvoudigere use-case: "zeg me wanneer er iets veranderd is." In plaats van steeds te vragen, kan je app wachten en reageren wanneer de database een klein signaal stuurt.
Het past goed bij UIs waar een duwtje voldoende is. Bijvoorbeeld:
De afweging is eenvoud versus garanties. LISTEN/NOTIFY is makkelijk toe te voegen omdat het al in Postgres zit, maar het is geen volwaardig berichtensysteem. De notificatie is een hint, geen duurzaam record. Als een listener losgekoppeld is, kan die het signaal missen.
Een praktische manier om het te gebruiken: laat NOTIFY je app wakker maken, en laat je app daarna de waarheid uit de tabellen lezen.
Zie LISTEN/NOTIFY als een eenvoudige deurbel ingebouwd in je database. Je app kan wachten tot de bel gaat, en een ander deel van je systeem kan erop drukken wanneer er iets verandert.
Een notificatie heeft twee delen: een kanaalnaam en een optionele payload. Het kanaal is als een topic-label (bijv. orders_changed). De payload is een korte tekstboodschap die je erbij hangt (bijv. een order-id). PostgreSQL legt geen structuur op, dus teams sturen vaak kleine JSON-strings.
Een notificatie kan worden getriggerd vanuit applicatiecode (je API-server draait NOTIFY) of vanuit de database zelf via een trigger (een trigger draait NOTIFY na een insert/update/delete).
Aan de ontvangende kant opent je app-server een databaseverbinding en voert LISTEN channel_name uit. Die verbinding blijft open. Wanneer er een NOTIFY channel_name, 'payload' gebeurt, pusht PostgreSQL een bericht naar alle verbindingen die naar dat kanaal luisteren. Je app reageert vervolgens (ververst cache, haalt de gewijzigde rij op, pusht een WebSocket-event naar de browser, enz.).
NOTIFY is het beste te begrijpen als een signaal, niet als een bezorgdienst:
Op deze manier kan PostgreSQL LISTEN/NOTIFY live UI-updates aandrijven zonder extra infrastructuur.
LISTEN/NOTIFY blinkt uit wanneer je UI alleen een duwtje nodig heeft dat er iets veranderde, niet een volledige eventstream. Denk "ververs dit widget" of "er is een nieuw item" in plaats van "verwerk elke klik in volgorde".
Het werkt het beste wanneer de database al je source of truth is en je de UI daarmee synchroon wilt houden. Een veelgebruikt patroon is: schrijf de rij, stuur een kleine notificatie met een ID, en laat de UI (of een API) de actuele staat ophalen.
LISTEN/NOTIFY is meestal voldoende wanneer het meeste van het volgende waar is:
Een concreet voorbeeld: een intern support-dashboard toont "open tickets" en een badge voor "nieuwe notities". Wanneer een agent een notitie toevoegt, schrijft je backend het naar Postgres en NOTIFYt ticket_changed met het ticket-ID. De browser ontvangt het via een WebSocket-verbinding en haalt dat ene ticketkaartje opnieuw op. Geen extra infrastructuur en de UI voelt nog steeds live.
LISTEN/NOTIFY voelt in het begin goed, maar heeft harde grenzen. Die komen naar voren als je notificaties behandelt als een berichtensysteem in plaats van als een licht "tik op de schouder".
De grootste kloof is duurzaamheid. Een NOTIFY is geen queued job. Als niemand luistert op dat moment, gaat het bericht verloren. Zelfs wanneer een listener verbonden is, kan een crash, deploy, netwerkonderbreking of databaseherstart de verbinding verbreken. Je krijgt niet automatisch de "gemiste" notificaties terug.
Disconnects zijn vooral pijnlijk voor gebruikersfuncties. Stel je een dashboard voor dat nieuwe bestellingen toont. Een browsertab kan slapen, de WebSocket herverbindt en de UI lijkt "vast" omdat een paar events gemist zijn. Je kunt hieromheen werken, maar die oplossing is niet langer "alleen LISTEN/NOTIFY": je bouwt de staat opnieuw op door de database te query'en en gebruikt NOTIFY alleen als hint om te verversen.
Fan-out is een ander veelvoorkomend probleem. Eén event kan honderden of duizenden luisteraars wakker maken (veel app-servers, veel gebruikers). Als je één luid kanaal gebruikt zoals orders, worden alle luisteraars wakker, ook al is maar één gebruiker geïnteresseerd. Dat kan pieken in CPU en connectiedruk veroorzaken op het slechtste moment.
Payload-grootte en frequentie zijn de laatste valkuilen. NOTIFY-payloads zijn klein en events met hoge frequentie kunnen zich sneller opstapelen dan clients kunnen verwerken.
Let op deze tekenen:
Op dat punt, houd NOTIFY als een "poke" en verplaats de betrouwbaarheid naar een tabel of een echte message broker.
Een betrouwbaar patroon met LISTEN/NOTIFY is om NOTIFY als een duwtje te behandelen, niet als bron van waarheid. De databasrij is de waarheid; de notificatie vertelt je app wanneer die moet kijken.
Doe de write binnen een transactie en stuur de notificatie pas nadat de wijziging is gecommit. Als je te vroeg notify't, kunnen clients wakker worden en de data nog niet vinden.
Een veelvoorkomende setup is een trigger die afgaat op INSERT/UPDATE en een klein bericht stuurt.
NOTIFY dashboard_updates, '{\\\"type\\\":\\\"order_changed\\\",\\\"order_id\\\":123}'::text;
Kanaalnamen werken het beste wanneer ze overeenkomen met hoe mensen over het systeem denken. Voorbeelden: dashboard_updates, user_notifications, of per-tenant zoals tenant_42_updates.
Houd de payload klein. Plaats identifiers en een type, geen volledige records. Een nuttige standaardvorm is:
type (wat er gebeurd is)id (wat veranderd is)tenant_id of user_idDit houdt bandbreedte laag en voorkomt dat gevoelige data in notificatielogs lekt.
Verbindingen vallen weg. Plan daarvoor.
Bij connect voer je LISTEN uit voor alle benodigde kanalen. Bij disconnect reconnect je met een korte backoff. Bij reconnect voer je opnieuw LISTEN uit (subscriptions blijven niet vanzelf bestaan). Na reconnect doe je een snelle refetch van "recente wijzigingen" om gemiste events te dekken.
Voor de meeste live UI-updates is refetchen de veiligste keuze: de client ontvangt {type, id} en vraagt de server dan om de laatste staat.
Incrementele patching kan sneller zijn, maar is makkelijker foutgevoelig (events komen mogelijk out-of-order, gedeeltelijke failures). Een goed middenweg is: refetch kleine slices (één orderrij, één ticketkaartje, één badge-telling) en laat zwaardere aggregaten op een korte timer draaien.
Wanneer je van één admin-dashboard naar veel gebruikers gaat die naar dezelfde cijfers kijken, worden goede gewoonten belangrijker dan slimme SQL. LISTEN/NOTIFY kan nog steeds goed werken, maar je moet vorm geven aan hoe events van de database naar browsers stromen.
Een veelvoorkomende baseline: elk app-instance opent één lange verbinding die LISTEN doet en vervolgens updates pusht naar verbonden clients. Deze "één listener per instance"-setup is simpel en vaak voldoende als je weinig app-servers hebt en incidentele reconnects kunt tolereren.
Heb je veel app-instanties (of serverless workers), dan is een gedeelde listener-service vaak makkelijker. Eén klein proces luistert eenmaal en fan-out naar de rest van je stack. Het geeft ook één plek voor batching, metrics en backpressure.
Voor browsers push je typisch met WebSockets (bidirectioneel, goed voor interactieve UIs) of Server-Sent Events (SSE) (éénrichtingsverkeer, eenvoudiger voor dashboards). Hoe dan ook, vermijd "ververs alles". Stuur compacte signalen zoals "order 123 changed" zodat de UI alleen opvraagt wat nodig is.
Om te voorkomen dat de UI trilt, voeg een paar beschermingen toe:
Kanaalontwerp doet er ook toe. In plaats van één globaal kanaal, partitioneer op tenant, team of feature zodat clients alleen relevante events ontvangen. Bijvoorbeeld: notify:tenant_42:billing en notify:tenant_42:ops.
LISTEN/NOTIFY voelt simpel, waardoor teams het snel uitrollen en later verrast raken. De meeste problemen ontstaan door het te behandelen als een gegarandeerde message queue.
Als je app herverbinden moet (deploy, netwerkstoring, DB failover), zijn alle NOTIFY-berichten die tijdens de disconnect gestuurd werden weg. De oplossing is de notificatie als signaal te zien en daarna de database opnieuw te controleren.
Een praktisch patroon: sla het echte event op in een tabel (met id en created_at), en bij reconnect haal je alles op dat nieuw is sinds je laatste gezien id.
LISTEN/NOTIFY-payloads zijn niet bedoeld voor grote JSON-blobs. Grote payloads betekenen extra werk, meer parsing en meer kans om limieten te raken.
Gebruik payloads voor kleine hints zoals "order:123". Laat de app daarna de actuele staat uit de database lezen.
Een veelgemaakte fout is de UI ontwerpen rondom de payloadinhoud alsof dat de waarheid is. Dat maakt schemawijzigingen en clientversies lastig.
Houd een duidelijke scheiding: notify dat iets veranderde en haal daarna de huidige data op met een gewone query.
Triggers die NOTIFY op elke rijwijziging doen, kunnen je systeem overspoelen, vooral op drukke tabellen.
Notify alleen bij betekenisvolle transities (bijv. statuswisselingen). Als je zeer lawaaiige updates hebt, batch veranderingen (één notify per transactie of per tijdvenster) of verplaats die updates uit het notify-pad.
Zelfs als de database notificaties kan sturen, kan je UI nog steeds bezwijken. Een dashboard dat bij elk event her-rendered kan vastlopen.
Debounce updates aan de clientzijde, voeg bursts samen tot één refresh en geef de voorkeur aan "invalidate and refetch" boven "pas elke delta toe". Bijvoorbeeld: een notificatiebel kan meteen updaten, maar de dropdownlijst ververst maximaal één keer per paar seconden.
LISTEN/NOTIFY is geweldig wanneer je een klein "er is iets veranderd"-signaal wilt zodat de app verse data kan ophalen. Het is geen volwaardig berichtensysteem.
Beantwoord deze vragen voordat je de UI eromheen bouwt:
LISTEN/NOTIFY meestal prima.Een praktische vuistregel: als je NOTIFY kunt behandelen als een duwtje ("ga de rij opnieuw lezen") in plaats van als de payload zelf, zit je in de veilige zone.
Voorbeeld: een admin-dashboard toont nieuwe orders. Als een notificatie gemist wordt, laat de volgende poll of pagina-refresh nog steeds het correcte aantal zien. Dat past goed. Maar als je "factureer deze kaart" of "verzend dit pakket" events stuurt, is het missen van één event een serieus incident.
Stel een kleine sales-app voor: een dashboard toont de omzet van vandaag, totaal aantal orders en een lijst "recente orders". Tegelijk moet elke verkoper snel een notificatie krijgen wanneer een order waar zij eigenaar van zijn is betaald of verzonden.
Een eenvoudige aanpak is Postgres als source of truth te houden en LISTEN/NOTIFY alleen te gebruiken als het tikje op de schouder dat er iets veranderd is.
Wanneer een order wordt aangemaakt of de status verandert, doet je backend twee dingen in één request: het schrijft of update de rij en stuurt daarna een NOTIFY met een kleine payload (vaak alleen het order-ID en event-type). De UI vertrouwt niet op de NOTIFY-payload voor alle data.
Een praktisch flow ziet er zo uit:
orders_events met {\\\"type\\\":\\\"status_changed\\\",\\\"order_id\\\":123}.Dit houdt NOTIFY lichtgewicht en beperkt dure queries.
Wanneer het verkeer groeit, ontstaan de beperkingen: een piek van events kan één listener overweldigen, notificaties kunnen gemist worden bij reconnects en je begint garanties en replay nodig te hebben. Meestal voeg je dan een betrouwbaardere laag toe (een outbox-tabel plus worker, daarna een broker indien nodig) terwijl Postgres de bron van waarheid blijft.
LISTEN/NOTIFY is prima voor een snel "er is iets veranderd"-signaal. Het is niet gebouwd als volwaardig berichtensysteem. Zodra je events als bron van waarheid gaat behandelen, is het tijd voor een broker.
LISTEN/NOTIFY gegroeid bentAls een van de volgende situaties verschijnt, bespaart een broker je veel pijn:
LISTEN/NOTIFY slaat geen berichten op voor later. Het is een push-signaal, geen gepersisteerde log. Dat is perfect voor "ververs dit dashboard-widget", maar riskant voor "start facturatie" of "verzend dit pakket".
Een broker geeft je een echt messageflow-model: queues (werk dat gedaan moet worden), topics (uitzenden naar velen), retentie (berichten bewaren van minuten tot dagen) en acknowledgments (een consumer bevestigt verwerking). Daarmee scheid je "de database is veranderd" van "alles wat moet gebeuren omdat het veranderde".
Je hoeft niet het meest complexe gereedschap te kiezen. Veelgehoorde opties zijn Redis (pub/sub of streams), NATS, RabbitMQ en Kafka. De juiste keuze hangt af van of je simpele work-queues, fan-out naar veel services of de mogelijkheid tot replay nodig hebt.
Je kunt migreren zonder grote herschrijving. Een praktisch patroon is NOTIFY als wake-up signaal te houden terwijl de broker de levering overneemt.
Begin met het schrijven van een "event-rij" in een tabel in dezelfde transactie als je business-change, en laat een worker dat event naar de broker publiceren. Tijdens de transitie kan NOTIFY nog steeds je UI-laag vertellen "check op nieuwe events", terwijl achtergrondworkers van de broker consumeren met retries en auditing.
Zo blijven dashboards snel en stoppen kritieke workflows met afhankelijk zijn van best-effort-notificaties.
Kies één scherm (een dashboard-tegel, een badge-telling, een "nieuwe notificatie"-toast) en sluit het end-to-end aan. Met LISTEN/NOTIFY kun je snel iets bruikbaars opleveren, zolang je de scope beperkt en meet wat er gebeurt onder echt verkeer.
Begin met het eenvoudigste betrouwbare patroon: schrijf de rij, commit en zend daarna een klein signaal dat er iets veranderde. In de UI reageer je op het signaal door de laatste staat op te halen (of het stukje dat je nodig hebt). Dit houdt payloads klein en voorkomt subtiele fouten wanneer berichten out-of-order aankomen.
Voeg vroeg basisobservability toe. Je hebt geen dure tools nodig, maar wel antwoorden wanneer het systeem lawaaiig wordt:
Houd contracten saai en gedocumenteerd. Bepaal kanaalnamen, event-namen en de vorm van eventuele payloads (ook al is het alleen een ID). Een korte "eventcatalogus" in je repo voorkomt drift.
Als je snel bouwt en de stack simpel wilt houden, kan een platform zoals Koder.ai je helpen de eerste versie met een React-UI, een Go-backend en PostgreSQL op te leveren, en daarna itereren naarmate je eisen duidelijker worden.
Gebruik LISTEN/NOTIFY wanneer je alleen een snelle signaal nodig hebt dat er iets gewijzigd is, bijvoorbeeld om een badge-telling of een dashboard-tegel te verversen. Zie de notificatie als een duwtje om de echte data uit de tabellen opnieuw te lezen, niet als de bron van waarheid.
Polling vraagt periodiek of er iets nieuw is, waardoor updates vaak vertraagd zijn en je server werk doet terwijl er niets verandert. LISTEN/NOTIFY duwt een klein signaal op het moment dat iets verandert, wat doorgaans sneller aanvoelt en veel lege verzoeken voorkomt.
Nee, het is best-effort. Als de listener tijdens een NOTIFY losgekoppeld is, kan die het signaal missen omdat notificaties niet worden opgeslagen voor latere replay.
Houd het klein en gebruik het als hint. Een praktisch default is een kleine JSON-string met een type en een id, en laat je app daarna de actuele status uit Postgres ophalen.
Een gebruikelijk patroon is om de notificatie pas te sturen nadat de write is gecommit. Als je te vroeg notify't, kan een client wakker worden en de nieuwe rij nog niet vinden.
Applicatiecode is meestal makkelijker te begrijpen en te testen omdat het expliciet is. Triggers zijn handig wanneer meerdere schrijvers dezelfde tabel raken en je consistent gedrag wilt ongeacht wie de wijziging maakte.
Plan voor reconnects als normaal gedrag. Bij reconnect run je weer LISTEN voor de benodigde kanalen en doe je een korte refetch van recente staat om te dekken wat mogelijk gemist is terwijl je offline was.
Laat niet elke browser rechtstreeks naar Postgres connecten. Een typische setup is één langlevende listener-connection per backend-instantie; die backend stuurt updates door naar browsers via WebSockets of SSE en de UI haalt daarna de benodigde data op.
Gebruik smallere kanalen zodat alleen de juiste consumers wakker worden, en batch ruige bursts. Debouncing van een paar honderd milliseconden en het samenvoegen van dubbele updates voorkomt dat UI en backend gaan thrashen.
Schakel over wanneer je duurzaamheid, retries, consumer groups, ordering guarantees of auditing/replay nodig hebt. Als het missen van een event een serieus incident veroorzaakt (bijv. facturatie of verzending), gebruik dan een outbox-tabel plus worker of een dedicated broker in plaats van alleen op NOTIFY te vertrouwen.