PostgreSQL LISTEN/NOTIFY может обеспечивать живые панели и оповещения с минимальной настройкой. Узнайте, где это подходит, какие ограничения и когда стоит добавить брокер.

«Живые обновления» в интерфейсе обычно означают, что экран меняется вскоре после события, без ручного обновления страницы. Число увеличилось на панели, на входящих появилось красное уведомление, админ увидел новый заказ или появился тост «Сборка завершена» или «Платёж провален». Важно время: это кажется мгновенным, даже если проходит секунда‑две.
Многие команды начинают с опроса: браузер каждые несколько секунд спрашивает сервер «есть ли что‑нибудь новое?». Опрос работает, но у него есть два типичных минуса.
Во‑первых, это кажется медленным, потому что пользователь увидит изменение только при следующем опросе.
Во‑вторых, это может дорого обходиться, потому что вы выполняете повторяющиеся проверки даже когда ничего не меняется. Умножьте это на тысячи пользователей — и это превращается в шум.
PostgreSQL LISTEN/NOTIFY предназначен для более простой задачи: «скажи мне, когда что‑то изменилось». Вместо того, чтобы спрашивать снова и снова, ваше приложение может ждать и среагировать, когда база пошлёт небольшой сигнал.
Он хорошо подходит для интерфейсов, где достаточно лёгкого толчка. Например:
Это компромисс между простотой и гарантиями. LISTEN/NOTIFY легко подключить, потому что он уже в PostgreSQL, но это не полноценная система обмена сообщениями. Уведомление — это подсказка, а не долговременная запись. Если слушатель отключён, он может пропустить сигнал.
Практический подход — давать NOTIFY «разбудить» приложение, а затем приложение читать фактические данные из таблиц.
Представьте LISTEN/NOTIFY как простой дверной звонок, встроенный в базу. Ваше приложение может ждать звонка, а другая часть системы звонит, когда что‑то меняется.
У уведомления две части: имя канала и опциональная полезная нагрузка. Канал похож на метку темы (например, orders_changed). Полезная нагрузка — короткое текстовое сообщение (например, идентификатор заказа). PostgreSQL не навязывает структуру, поэтому команды часто посылают маленькие JSON‑строки.
NOTIFY может вызываться из кода приложения (ваш API вызывает NOTIFY) или из самой базы данных через триггер (триггер вызывает NOTIFY после INSERT/UPDATE/DELETE).
На принимающей стороне сервер приложения открывает соединение с базой и выполняет LISTEN channel_name. Это соединение остаётся открытым. Когда выполняется NOTIFY channel_name, 'payload', PostgreSQL отправляет сообщение всем соединениям, слушающим этот канал. Затем приложение реагирует (обновляет кэш, читает изменённую строку, посылает событие по WebSocket в браузер и так далее).
NOTIFY лучше понимать как сигнал, а не сервис доставки:
Используемый таким образом PostgreSQL LISTEN/NOTIFY способен обеспечить живые обновления UI без дополнительной инфраструктуры.
LISTEN/NOTIFY отлично подходит, когда вашему интерфейсу нужен только толчок о том, что что‑то изменилось, а не полный поток событий. Думайте «обнови этот виджет» или «появился новый элемент», а не «обрабатывай каждый клик по порядку».
Он работает лучше, когда база уже — источник правды, и вы хотите, чтобы UI оставался синхронизированным с ней. Частый шаблон: записать строку, отправить небольшое уведомление с ID, и UI (или API) запрашивает актуальное состояние.
Обычно LISTEN/NOTIFY достаточно, когда верно большинство из следующих пунктов:
Конкретный пример: внутренний дашборд показывает «открытые тикеты» и бейдж «новые заметки». Когда агент добавляет заметку, backend записывает её в Postgres и выполняет NOTIFY ticket_changed с ID тикета. Браузер получает это через WebSocket и перезапрашивает карточку тикета. Никакой лишней инфраструктуры, но интерфейс по‑прежнему выглядит живым.
Сначала LISTEN/NOTIFY может работать отлично, но у него есть жёсткие ограничения. Они проявляются, когда вы пользуетесь уведомлениями как системой сообщений, а не как лёгким «потыкать в плечо».
Самый большой пробел — надёжность. NOTIFY — не очередь задач. Если никто не слушает в момент отправки, сообщение теряется. Даже когда слушатель подключён, крах, деплой, сетевой сбой или перезапуск базы могут оборвать соединение. Вы не получите автоматически «пропущенные» уведомления.
Отключения особенно болезненны для пользовательских фич. Представьте дашборд с новыми заказами. Вкладка браузера заснула, WebSocket переподключился, а UI «завис», потому что он пропустил события. С этим можно справиться, но обход перестаёт быть просто LISTEN/NOTIFY: приходится восстанавливаться, опрашивая базу, а NOTIFY используется только как подсказка.
Ещё одна проблема — fan‑out. Одно событие может разбудить сотни или тысячи слушателей (множество серверов, множество пользователей). Если вы используете один шумный канал вроде orders, все слушатели проснутся, даже если событие интересно только одному пользователю. Это может создать всплески нагрузки в худший момент.
Размер полезной нагрузки и частота — последние ловушки. Полезные нагрузки у NOTIFY маленькие, а события с высокой частотой могут накапливаться быстрее, чем клиенты успевают их обрабатывать.
Следите за такими симптомами:
В такой ситуации оставляйте NOTIFY как «похлопывание по плечу», а надёжность переносите в таблицу или полноценный брокер сообщений.
Надёжный паттерн с LISTEN/NOTIFY — считать NOTIFY подсказкой, а не источником правды. Строка в базе — источник правды; уведомление говорит приложению, когда её перечитать.
Делайте запись внутри транзакции и шлите уведомление только после коммита. Если нотифицировать раньше, клиенты могут проснуться и не найти данные.
Часто используют триггер, который срабатывает на INSERT/UPDATE и отправляет маленькое сообщение.
NOTIFY dashboard_updates, '{"type":"order_changed","order_id":123}'::text;
Имена каналов удобно выбирать так, как вы думаете о системе: dashboard_updates, user_notifications или по‑арендаторные tenant_42_updates.
Держите полезную нагрузку маленькой. Помещайте идентификаторы и тип, а не полные записи. Удобная форма:
type (что произошло)id (что изменилось)tenant_id или user_idЭто снижает трафик и не даёт случайно записать конфиденциальные данные в логи уведомлений.
Соединения обрываются. Планируйте это.
При подключении выполните LISTEN для всех нужных каналов. При отключении переподключайтесь с коротким бэкоффом. После переподключения снова выполните LISTEN (подписки не сохраняются). После переподключения быстро перечитайте «недавние изменения», чтобы покрыть пропущенные события.
Для большинства живых обновлений безопаснее всего делать повторный запрос: клиент получает {type, id}, затем запрашивает у сервера текущее состояние.
Инкрементальные патчи могут быть быстрее, но их проще сломать (события вне порядка, частичные ошибки). Хороший компромисс — повторно запрашивать небольшие срезы (одна строка заказа, одна карточка тикета, один счётчик) и оставлять тяжёлые агрегаты на коротком таймере.
Когда у вас один админ‑дашборд меняется на множество пользователей, правильные практики важнее умных SQL‑трюков. LISTEN/NOTIFY всё ещё может работать, но нужно формировать поток от базы к браузерам.
Обычная базовая схема: каждый экземпляр приложения держит одно длительное соединение и LISTEN, затем пересылает обновления подключённым клиентам. Эта «одна подписка на экземпляр» проста и часто подходит, если у вас немного серверов и вы терпите редкие переподключения.
Если у вас много инстансов (или бессерверные воркеры), удобнее иметь отдельный сервис‑слушатель. Один процесс слушает и делает fan‑out в остальную часть стека. Там же легко добавить батчинг, метрики и контроль нагрузки.
Для браузеров обычно используют WebSocket (двунаправленный, хорош для интерактивного UI) или Server‑Sent Events (однонаправленный, проще для дашбордов). В любом случае избегайте «обнови всё». Шлите компактные сигналы вроде «order 123 changed», чтобы UI мог запросить только нужное.
Чтобы UI не дергался слишком часто, добавьте ограничения:
Дизайн каналов тоже важен. Вместо одного глобального канала разделяйте по арендаторам, командам или фичам, чтобы клиенты получали только релевантные события. Пример: notify:tenant_42:billing и notify:tenant_42:ops.
LISTEN/NOTIFY кажется простым, поэтому команды быстро его выкатывают, а затем удивляются в продакшене. Большинство проблем возникает из попытки использовать его как гарантирующую очередь сообщений.
Если ваше приложение переподключается (деплой, сетевая проблема, failover), любой NOTIFY, отправленный в период отключения, будет утерян. Исправление — считать уведомление сигналом и затем перепроверять базу.
Практичный паттерн: сохранять реальное событие в таблице (с id и created_at), а при переподключении запрашивать всё новее последнего увиденного id.
Полезные нагрузки NOTIFY не рассчитаны на большие JSON‑блоки. Большие полезные нагрузки создают лишнюю работу, парсинг и вероятность достижения лимитов.
Используйте полезную нагрузку как лёгкую подсказку, например "order:123". Затем приложение читает актуальное состояние из базы.
Ошибка — строить UI вокруг содержимого полезной нагрузки, будто это источник правды. Это делает изменения схемы и поддержку старых версий клиентов сложной.
Разделяйте ответственность: нотификация говорит, что что‑то сменилось, а данные читаются обычным запросом.
Триггеры, которые делают NOTIFY на каждое изменение строки, могут зафлудить систему, особенно для горячих таблиц.
Нотифицируйте только при значимых переходах (например, смена статуса). Если много шума, батчируйте изменения (один NOTIFY на транзакцию или окно времени) или вынесите эти обновления из пути нотификации.
Даже если база может посылать уведомления, ваш интерфейс может не справиться. Дашборд, который перерисовывается на каждое событие, может зависнуть.
Дебаунсьте обновления на клиенте, объединяйте всплески в одно обновление и предпочитайте «инвалидировать и перезапросить» вместо «применить каждое дельта‑изменение». Например: бейдж уведомления может обновляться мгновенно, а список — не чаще раза в несколько секунд.
LISTEN/NOTIFY хорош, если вам нужен небольшой сигнал «что‑то изменилось», чтобы приложение могло запросить свежие данные. Это не полноценная система сообщений.
Прежде чем строить UI вокруг неё, ответьте на вопросы:
Практическое правило: если вы можете считать NOTIFY подсказкой («перечитай строку») вместо источника данных, вы в безопасной зоне.
Пример: админ‑дашборд показывает количество новых заказов. Если нотификация пропущена, следующий опрос или обновление страницы всё равно покажет правильный счёт. Это подходящий случай. Но если вы отправляете «списать деньги с карты» или «отправить посылку», пропуск события — серьёзная проблема.
Представьте простое приложение продаж: панель показывает выручку за сегодня, общее число заказов и список «последних заказов». Одновременно продавцы должны получать уведомления, когда их заказ оплачен или отправлен.
Простой подход — считать PostgreSQL источником правды и использовать LISTEN/NOTIFY только как похлопывание по плечу.
Когда заказ создаётся или меняет статус, backend в одном запросе делает два шага: записывает или обновляет строку и после этого отправляет небольшой NOTIFY (обычно только ID заказа и тип события). UI не полагается на полезную нагрузку для всех данных.
Практический поток выглядит так:
orders_events с {\"type\":\"status_changed\",\"order_id\":123}.Это держит NOTIFY лёгким и ограничивает дорогие запросы.
Когда трафик растёт, проявляются проблемы: всплеск событий может перегрузить одного слушателя, уведомления теряются при переподключении, и вам понадобится гарантия доставки и воспроизведения. Обычно тогда добавляют надёжный слой (outbox‑таблица и воркер, затем брокер при необходимости), сохраняя Postgres как источник правды.
LISTEN/NOTIFY хорош для быстрого сигнала «что‑то изменилось». Это не система обмена сообщениями. Когда вы начинаете полагаться на события как на источник правды, пора добавить брокер.
Если появилось любое из следующего, брокер спасёт вас от проблем:
LISTEN/NOTIFY не хранит сообщения для позднего чтения. Это push‑сигнал, а не сохранённый лог. Это отлично для «обнови виджет», но рискованно для «списать платёж» или «отправить посылку».
Брокер даёт реальную модель потоков сообщений: очереди (работа, которую нужно сделать), топики (трансляция многим), хранение (сохранение сообщений от минут до дней) и подтверждения (потребитель подтверждает обработку). Это позволяет отделить «база изменилась» от «всё, что должно произойти из‑за этого изменения».
Не обязательно брать самый сложный инструмент. Популярные опции: Redis (pub/sub или streams), NATS, RabbitMQ, Kafka. Выбор зависит от того, нужны ли простые очереди задач, масштабируемый fan‑out или возможность воспроизведения истории.
Перевод можно делать без большого рефактора. Практичный паттерн — оставить NOTIFY как сигнал, пока брокер берёт на себя доставку.
Начните с записи «строки события» в таблицу в той же транзакции, что и бизнес‑изменение, затем воркер публикует это событие в брокер. В переходный период NOTIFY может по‑прежнему говорить UI «проверь новые события», а фоновые воркеры потребляют из брокера с ретраями и аудитом.
Так дашборды остаются отзывчивыми, а критические рабочие процессы перестают зависеть от best‑effort уведомлений.
Выберите один экран (плитка на дашборде, счётчик, тост) и пропишите его от начала до конца. С LISTEN/NOTIFY можно быстро получить полезный результат, если ограничить объём и измерять поведение под реальной нагрузкой.
Начните с простого надёжного шаблона: записать строку, закоммитить, затем отправить маленький сигнал. В UI реагируйте на сигнал повторным запросом нужного среза. Это держит полезные нагрузки маленькими и уменьшает баги при приходе сообщений вне порядка.
Ранний мониторинг обязателен. Не нужны сложные инструменты, но нужны ответы, когда система начинает шуметь:
Держите контракты простыми и записанными. Решите имена каналов, названия событий и форму полезной нагрузки (хотя бы type/id). Небольшой «каталог событий» в репозитории предотвратит дрейф.
Если вы хотите быстро собрать и не наращивать стек, платформа Koder.ai может помочь вам выпустить первую версию с React UI, Go backend и PostgreSQL, а затем итеративно усложнять архитектуру по мере роста требований.
Используйте LISTEN/NOTIFY, когда вам нужен быстрый сигнал о том, что что‑то изменилось — например, чтобы обновить счётчик или плитку на панели. Рассматривайте уведомление как подсказку для повторного запроса реальных данных из таблиц, а не как источник самих данных.
Опрос проверяет изменения по расписанию, поэтому пользователи видят обновления с задержкой, а сервер выполняет запросы даже когда ничего не меняется. LISTEN/NOTIFY отправляет небольшой сигнал в момент изменения, что обычно чувствуется быстрее и экономит лишние запросы.
Нет — это best‑effort. Если слушатель был отключён во время NOTIFY, он может пропустить сигнал, потому что уведомления не сохраняются для последующего воспроизведения.
Держите полезную нагрузку минимальной и используйте её как подсказку. Полезная форма по умолчанию — небольшой JSON с type и id, после чего приложение запрашивает актуальное состояние из Postgres.
Обычно уведомление отправляют после фиксации транзакции. Если нотифицировать до коммита, клиент может проснуться и не найти ещё новую строку.
Код приложения легче отслеживать и тестировать — всё явно. Триггеры полезны, когда многие писатели меняют одну таблицу и вы хотите единообразного поведения независимо от того, кто сделал изменение.
Планируйте переподключения как обычную ситуацию. При повторном подключении снова выполните LISTEN для нужных каналов и быстро запросите недавнее состояние, чтобы покрыть возможные пропуски.
Не подключайте каждый браузер к Postgres. Обычно каждое бэкенд‑приложение держит одно длительное соединение‑слушатель и пересылает события браузерам через WebSocket или SSE; затем UI сам запрашивает нужные данные.
Сужайте каналы, чтобы тревожились только нужные получатели, и пакетируйте шумные всплески. Дебаунс на сотни миллисекунд и стягивание дубликатов помогают не перегружать UI и backend.
Переходите, когда нужна надёжность, ретраи, группы потребителей, строгое упорядочивание или аудит/воспроизведение. Если пропущенное событие может привести к инциденту (выставление счета, отправка заказа), используйте outbox‑таблицу и воркера или полноценный брокер вместо опоры только на NOTIFY.