PostgreSQL LISTEN/NOTIFY kann Live‑Dashboards und Alerts mit minimalem Aufwand antreiben. Erfahre, wo es passt, seine Grenzen und wann du einen Broker ergänzen solltest.

„Live‑Updates“ in einer Produkt‑UI bedeuten meist, dass sich der Bildschirm kurz nach einer Änderung ohne manuelles Neuladen aktualisiert. Eine Zahl steigt auf dem Dashboard, ein roter Badge erscheint im Posteingang, ein Admin sieht eine neue Bestellung oder ein Toast zeigt „Build finished“ oder „Payment failed“. Entscheidend ist der Eindruck der Schnelligkeit: es fühlt sich sofort an, auch wenn es tatsächlich eine Sekunde oder zwei dauert.
Viele Teams starten mit Polling: der Browser fragt den Server alle paar Sekunden „gibt es etwas Neues?“. Polling funktioniert, hat aber zwei häufige Nachteile.
Erstens wirkt es träge, weil Nutzer Änderungen erst beim nächsten Poll sehen.
Zweitens kann es teuer werden, weil wiederholt geprüft wird, selbst wenn sich nichts geändert hat. Multipliziert sich das auf tausende Nutzer, entsteht schlicht Noise.
PostgreSQL LISTEN/NOTIFY gibt es für einen einfacheren Anwendungsfall: „sag mir, wenn sich etwas geändert hat.“ Anstatt immer wieder zu fragen, kann deine App warten und reagieren, wenn die Datenbank ein kleines Signal sendet.
Es passt gut für UIs, bei denen ein kleiner Anstoß reicht. Zum Beispiel:
Der Kompromiss ist Einfachheit versus Garantien. LISTEN/NOTIFY ist leicht hinzuzufügen, weil es bereits in Postgres integriert ist, aber es ist kein vollwertiges Messaging‑System. Die Benachrichtigung ist ein Hinweis, keine dauerhafte Aufzeichnung. Wenn ein Listener getrennt ist, kann er das Signal verpassen.
Eine praktische Nutzung ist: NOTIFY weckt deine App auf, danach liest deine App die Wahrheit aus den Tabellen.
Stell dir PostgreSQL LISTEN/NOTIFY als eine einfache Türklingel in deiner Datenbank vor. Deine App kann warten, bis die Klingel läutet, und ein anderer Teil des Systems kann läuten, wenn sich etwas ändert.
Eine Benachrichtigung besteht aus zwei Teilen: einem Channel‑Namen und einer optionalen Payload. Der Channel ist wie ein Topic‑Label (z. B. orders_changed). Die Payload ist eine kurze Textnachricht (z. B. eine Order‑ID). PostgreSQL erzwingt keine Struktur, weshalb Teams oft kleine JSON‑Strings senden.
Eine Benachrichtigung kann aus Anwendungscode ausgelöst werden (dein API‑Server führt NOTIFY aus) oder direkt in der Datenbank über einen Trigger (ein Trigger führt nach INSERT/UPDATE/DELETE NOTIFY aus).
Auf der Empfangsseite öffnet dein App‑Server eine DB‑Verbindung und führt LISTEN channel_name aus. Diese Verbindung bleibt offen. Wenn ein NOTIFY channel_name, 'payload' erfolgt, pusht Postgres eine Nachricht an alle Verbindungen, die auf diesem Channel lauschen. Deine App reagiert dann (Cache invalidieren, die geänderte Zeile abfragen, ein WebSocket‑Event an den Browser senden usw.).
NOTIFY ist eher ein Signal als ein Zustelldienst:
Auf diese Weise kann PostgreSQL LISTEN/NOTIFY Live‑UI‑Updates ermöglichen, ohne zusätzliche Infrastruktur.
LISTEN/NOTIFY glänzt, wenn deine UI nur einen Anstoß braucht, dass sich etwas geändert hat, nicht einen vollständigen Event‑Stream. Denke „aktualisiere dieses Widget“ oder „es gibt ein neues Element“ statt „verarbeite jeden Klick in Reihenfolge“.
Es funktioniert am besten, wenn die Datenbank bereits deine Source of Truth ist und du die UI damit synchron halten willst. Ein gängiges Muster: Zeile schreiben, eine kleine Benachrichtigung mit einer ID senden, und die UI (oder ein API) holt den aktuellen Zustand.
LISTEN/NOTIFY reicht normalerweise aus, wenn die meisten der folgenden Punkte zutreffen:
Ein konkretes Beispiel: Ein internes Support‑Dashboard zeigt „open tickets“ und ein Badge für „neue Notizen“. Wenn ein Agent eine Notiz hinzufügt, schreibt das Backend sie in Postgres und NOTIFYt ticket_changed mit der Ticket‑ID. Der Browser empfängt das über WebSocket und holt genau diese Ticket‑Karte nach. Keine zusätzliche Infrastruktur, und die UI wirkt live.
LISTEN/NOTIFY fühlt sich anfangs großartig an, aber es hat harte Grenzen. Diese zeigen sich, wenn du Notifications wie ein Nachrichtensystem behandelst statt als leichtes „Klopfen an die Schulter“.
Die größte Lücke ist Haltbarkeit. Ein NOTIFY ist kein queued job. Wenn niemand lauscht, ist die Nachricht weg. Selbst wenn ein Listener verbunden ist, können Crashs, Deploys, Netzwerkprobleme oder ein DB‑Restart die Verbindung unterbrechen. Verpasste Benachrichtigungen werden nicht automatisch nachgeliefert.
Disconnects sind besonders problematisch für nutzerorientierte Features. Stell dir ein Dashboard für neue Bestellungen vor: Ein Browser‑Tab schläft, das WebSocket reconnectet und die UI wirkt „eingefroren“, weil ein paar Events verpasst wurden. Das lässt sich umgehen, aber dann ist die Lösung nicht mehr „nur LISTEN/NOTIFY“: du musst den Zustand wiederherstellen, indem du die DB abfragst und NOTIFY nur als Hinweis verwendest.
Fan‑out ist ein weiteres Problem. Ein Event kann hunderte oder tausende Listener wecken (viele App‑Server, viele Nutzer). Wenn du einen lauten Channel wie orders benutzt, wachen alle auf, auch wenn nur ein Nutzer das Event braucht. Das kann zu Lastspitzen und Verbindungsdruck führen.
Payload‑Größe und Frequenz sind die letzten Fallen. NOTIFY‑Payloads sind klein, und hochfrequente Events können sich schneller stapeln, als Clients verarbeiten können.
Achte auf diese Warnsignale:
Dann gilt: Behalte NOTIFY als „Stups“ und verlagere Zuverlässigkeit in eine Tabelle oder einen echten Message‑Broker.
Ein verlässliches Muster ist, NOTIFY als Anstoß, nicht als Quelle der Wahrheit zu verwenden. Die Datenbankzeile ist die Wahrheit; die Benachrichtigung sagt deiner App, wann sie nachschauen soll.
Führe den Schreibvorgang in einer Transaktion aus und sende die Benachrichtigung erst nach dem Commit. Wenn du zu früh notifyst, wachen Clients auf und finden die Daten eventuell noch nicht.
Gängige Einrichtung: ein Trigger, der bei INSERT/UPDATE feuert und eine kleine Nachricht sendet.
NOTIFY dashboard_updates, '{\\\"type\\\":\\\"order_changed\\\",\\\"order_id\\\":123}'::text;
Channel‑Namen funktionieren am besten, wenn sie zur Denkweise über das System passen. Beispiele: dashboard_updates, user_notifications oder pro Tenant wie tenant_42_updates.
Halte die Payload klein. Sende Identifikatoren und einen Typ, nicht ganze Datensätze. Eine nützliche Standard‑Form ist:
type (was passiert ist)id (was sich geändert hat)tenant_id oder user_idDas reduziert Bandbreite und vermeidet das Leaken sensibler Daten in Logs.
Verbindungen fallen aus. Plane dafür.
Beim Verbinden LISTEN für alle benötigten Channels ausführen. Bei Disconnect neu verbinden mit kurzem Backoff. Nach Reconnect erneut LISTEN ausführen (Subscriptions werden nicht automatisch übernommen). Nach dem Reconnect kurz die „recent changes“ nachladen, um verpasste Events abzudecken.
Für die meisten Live‑UI‑Updates ist Nachladen die sicherste Methode: Der Client erhält {type, id} und fragt dann den Server nach dem neuesten Zustand.
Inkrementelles Patchen kann schneller sein, ist aber leichter fehleranfällig (Events in falscher Reihenfolge, Teil‑Fehler). Ein guter Kompromiss: kleine Slices neu laden (eine Order‑Zeile, eine Ticket‑Karte, eine Badge‑Zahl) und umfangreichere Aggregationen im kurzen Takt nachziehen.
Wenn du von einem Admin‑Dashboard zu vielen Nutzern wechselst, die dieselben Zahlen beobachten, werden gute Gewohnheiten wichtiger als clevere SQL‑Tricks. LISTEN/NOTIFY kann weiterhin gut funktionieren, aber du musst den Event‑Flow von der DB zu den Browsern formen.
Ein übliches Grundmuster: Jede App‑Instanz öffnet eine lang lebende Verbindung, die LISTEN ausführt, und pusht Updates an verbundene Clients. Dieses „ein Listener pro Instanz“ ist simpel und oft ausreichend, wenn du wenige App‑Server hast und gelegentliche Reconnects tolerierbar sind.
Hast du viele Instanzen (oder serverlose Worker), ist ein geteilter Listener‑Service hilfreicher. Ein kleiner Prozess lauscht einmal und fanned Updates an den Rest der Infrastruktur aus. Dort kannst du Batching, Metriken und Backpressure zentralisieren.
Für Browser nutzt du typischerweise WebSockets (bidirektional, gut für interaktive UIs) oder Server‑Sent Events (SSE) (unidirektional, simpler für Dashboards). Vermeide „alles neu laden“. Sende kompakte Signale wie „order 123 changed“, damit die UI nur das nachlädt, was nötig ist.
Um Thrashing zu vermeiden, baue ein paar Guardrails ein:
Channel‑Design ist ebenfalls wichtig. Statt eines globalen Channels partitioniere nach Tenant, Team oder Feature, damit Clients nur relevante Events erhalten. Beispiel: notify:tenant_42:billing und notify:tenant_42:ops.
LISTEN/NOTIFY wirkt simpel, daher wird es schnell ausgeliefert — und Teams wundern sich später in Produktion. Die meisten Probleme entstehen, wenn man es wie eine garantierte Message‑Queue behandelt.
Wenn deine App reconnectet (Deploy, Network Blip, DB Failover), sind während der Trennung gesendete NOTIFYs weg. Die Lösung: Benachrichtigung als Signal behandeln und den DB‑Zustand erneut prüfen.
Praktisches Muster: Das eigentliche Event in einer Tabelle speichern (mit ID und created_at), und nach Reconnect alles nachladen, was neuer ist als die zuletzt gesehene ID.
LISTEN/NOTIFY‑Payloads sind nicht für große JSON‑Blobs gedacht. Große Payloads verursachen zusätzlichen Aufwand, Parsing und erhöhen die Gefahr, Limits zu erreichen.
Nutze Payloads für kleine Hinweise wie "order:123". Die App liest dann den aktuellen Zustand aus der DB.
Ein häufiger Fehler ist, das UI um die Payload herum zu bauen, als wäre sie die Quelle der Wahrheit. Das macht Schema‑Änderungen und Client‑Versionen kompliziert.
Trenne klar: notify, dass sich etwas geändert hat, und hol dann die aktuellen Daten per normaler Query.
Trigger, die bei jeder Zeilenänderung NOTIFY auslösen, können das System fluten, besonders bei stark frequentierten Tabellen.
Notify nur bei sinnvollen Übergängen (z. B. Statuswechsel). Bei sehr lauten Updates batchen (ein Notify pro Transaction oder pro Zeitfenster) oder verschiebe die Updates aus dem Notify‑Pfad.
Auch wenn die DB Notifications senden kann, kann deine UI trotzdem überlasten. Ein Dashboard, das bei jedem Event neu rendert, kann einfrieren.
Debounce Updates im Client, fasse Bursts zu einem Refresh zusammen und bevorzuge „invalidate and refetch“ gegenüber dem Anwenden jedes Deltas. Beispiel: Die Glocke kann sofort updaten, aber die Dropdown‑Liste refreshed höchstens alle paar Sekunden.
LISTEN/NOTIFY ist ideal, wenn du ein kleines „etwas hat sich geändert“‑Signal willst, damit die App frische Daten holen kann. Es ist kein komplettes Messaging‑System.
Beantworte vor dem Aufbau der UI diese Fragen:
Praktische Regel: Wenn du NOTIFY als Stups („geh die Zeile neu lesen“) nutzen kannst, bist du im sicheren Bereich.
Beispiel: Ein Admin‑Dashboard zeigt neue Bestellungen. Wird eine Notification verpasst, zeigt der nächste Poll oder ein Page‑Refresh trotzdem die korrekte Zahl. Das passt. Schickst du hingegen „Belaste diese Karte“ oder „Versende dieses Paket“, ist ein verpasstes Event ein echter Vorfall.
Stell dir eine kleine Sales‑App vor: Ein Dashboard zeigt Tagesumsatz, Gesamtbestellungen und eine Liste „recent orders“. Gleichzeitig soll jeder Verkäufer eine kurze Benachrichtigung erhalten, wenn eine Bestellung, für die er verantwortlich ist, bezahlt oder versandt wurde.
Ein einfacher Ansatz ist, Postgres als Source of Truth zu behandeln und LISTEN/NOTIFY nur als Stups zu verwenden.
Wenn eine Bestellung erstellt oder ihr Status geändert wird, macht dein Backend in einer Anfrage zwei Dinge: es schreibt/updated die Zeile und sendet danach ein kleines NOTIFY (oft nur Order‑ID und Event‑Typ). Die UI verlässt sich nicht auf den Payload als vollständige Datenquelle.
Ein praktischer Ablauf:
NOTIFY orders_events mit {\\\"type\\\":\\\"status_changed\\\",\\\"order_id\\\":123}.So bleibt NOTIFY leichtgewichtig und teure Queries bleiben begrenzt.
Wächst der Traffic, treten Risse auf: Event‑Spitzen können einen einzelnen Listener überwältigen, Notifications gehen beim Reconnect verloren und du brauchst dann garantierte Zustellung und Replay. Dann ergänzt du meist eine robustere Schicht (Outbox‑Tabelle plus Worker, später ein Broker) und behältst Postgres als Source of Truth.
LISTEN/NOTIFY ist ideal für ein schnelles „etwas hat sich geändert“‑Signal. Es ist nicht als komplettes Messaging‑System gebaut. Sobald Events für dich zur Quelle der Wahrheit werden, ist es Zeit für einen Broker.
Ein Broker hilft, wenn eines der folgenden Probleme auftritt:
LISTEN/NOTIFY speichert Nachrichten nicht für später. Es ist ein Push‑Signal, kein persistentes Log. Perfekt für „aktualisiere dieses Dashboard‑Widget“, riskant für „rechne diese Zahlung ab“ oder „versende dieses Paket“.
Ein Broker gibt dir ein echtes Message‑Flow‑Modell: Queues (Arbeit), Topics (Broadcast), Retention (Nachrichten für Minuten bis Tage speichern) und Acknowledgements (Konsument bestätigt Verarbeitung). So trennst du „DB hat sich geändert“ von „alles, was aufgrund dieser Änderung passieren muss“.
Du musst nicht das komplexeste Tool wählen. Häufig bewertet man Redis (pub/sub oder Streams), NATS, RabbitMQ oder Kafka. Die richtige Wahl hängt davon ab, ob du einfache Work‑Queues, Fan‑out an viele Services oder Replay‑Fähigkeit brauchst.
Du kannst schrittweise wechseln. Ein praktikables Muster: Behalte NOTIFY als Wake‑Up‑Signal, während der Broker zur Lieferquelle wird.
Schreibe zuerst eine „Event‑Zeile“ in eine Tabelle innerhalb derselben Transaktion wie die Business‑Änderung, und lass einen Worker dieses Event an den Broker publizieren. Während der Übergangsphase kann NOTIFY weiterhin deine UI‑Schicht „prüfe auf neue Events“ wecken, während Hintergrund‑Worker vom Broker mit Retries und Auditing konsumieren.
So bleiben Dashboards flüssig und kritische Workflows verlassen sich nicht mehr auf Best‑Effort‑Benachrichtigungen.
Wähle einen Screen (eine Dashboard‑Kachel, einen Badge‑Zähler, einen „neue Benachrichtigung“‑Toast) und verdrahte ihn Ende‑zu‑Ende. Mit LISTEN/NOTIFY kannst du schnell ein brauchbares Ergebnis liefern, solange du den Umfang eng hältst und unter realem Traffic misst.
Beginne mit dem einfachsten verlässlichen Muster: Zeile schreiben, committen und dann ein kleines Signal aussenden. Im UI reagierst du auf das Signal, indem du den neuesten Zustand nachlädst (oder den nötigen Ausschnitt). So bleiben Payloads klein und du vermeidest subtile Fehler bei aus‑der‑Reihenfolge eintreffenden Nachrichten.
Baue früh Grund‑Observability ein. Du brauchst keine aufwendigen Tools, aber Antworten, wenn das System laut wird:
Halte Contracts langweilig und dokumentiert. Lege Channel‑Namen, Event‑Namen und die Form jeder Payload fest (auch wenn es nur eine ID ist). Ein kurzes „Event‑Katalog“ im Repo verhindert Drift.
Wenn du schnell bauen willst und den Stack einfach halten möchtest, kann eine Plattform wie Koder.ai (koder.ai) dir helfen, die erste Version mit React UI, Go‑Backend und PostgreSQL auszuliefern und dann zu iterieren, sobald die Anforderungen klarer werden.
Verwende LISTEN/NOTIFY, wenn du nur ein kurzes Signal brauchst, dass sich etwas geändert hat — etwa um eine Badge‑Zahl oder ein Dashboard‑Kachel zu aktualisieren. Behandle die Benachrichtigung als Anstoß, die echten Daten aus den Tabellen nachzufragen, nicht als Quelle der Wahrheit.
Polling fragt in festen Intervallen nach Änderungen, sodass Nutzer oft verspätete Updates sehen und dein Server auch dann arbeitet, wenn nichts neu ist. LISTEN/NOTIFY sendet ein kleines Signal genau dann, wenn sich etwas ändert — das fühlt sich meist schneller an und vermeidet viele unnötige Requests.
Nein. LISTEN/NOTIFY ist best‑effort. Wenn der Listener während eines NOTIFY getrennt ist, kann das Signal verloren gehen, weil Benachrichtigungen nicht zur späteren Wiedergabe gespeichert werden.
Halte die Payload klein und nutze sie als Hinweis. Ein praktisches Default ist ein kleines JSON mit type und id; die Anwendung holt dann den aktuellen Zustand aus der Datenbank.
Gängiges Muster: Benachrichtigung nach dem Commit senden. Wenn du zu früh notifyst, kann ein Client aufwachen und die neue Zeile noch nicht finden.
Application‑Code ist meist leichter nachzuvollziehen und zu testen, weil es explizit ist. Trigger sind praktisch, wenn viele verschiedene Schreibroutinen dieselben Tabellen ändern und ein konsistentes Verhalten gewünscht ist.
Plane Reconnects als normalen Zustand ein. Bei Verbindungsetablierung LISTEN für alle benötigten Channels ausführen und bei Reconnect kurz den jüngsten Zustand nachladen, um mögliche verpasste Events abzudecken.
Nicht jeder Browser sollte direkt mit Postgres verbunden sein. Üblicher Aufbau: pro Backend‑Instanz eine lang lebende Listener‑Verbindung, die Events an Browser via WebSocket oder SSE weiterreicht; das UI holt sich dann die benötigten Daten nach.
Verwende engere Channels, damit nur die relevanten Konsumenten aufwachen, und batche laute Bursts. Debounce‑Zeiten von einigen Hundert Millisekunden und das Zusammenfassen doppelter Updates verhindern, dass UI und Backend überlasten.
Wechsel, wenn du Haltbarkeit, Retries, Consumer‑Gruppen, Reihenfolgen oder Audit/Replay brauchst. Wenn ein verpasstes Event echte Vorfälle (z. B. Abrechnung, Versand) auslöst, nutze lieber eine Outbox plus Worker oder einen dedizierten Broker anstelle von NOTIFY alleine.