Skip to content

Node-Restricted Channels

Some actions have node-local side effects: they change state that logical replication does not carry between nodes, so "the same event" run on two different nodes is actually two different pieces of physical work. The classic example is a materialised view refresh — a materialised view's underlying storage is not logically replicated, every node holds its own separate copy, and only a local REFRESH can update it. A companion application (for example, pg_auto_mv) deliberately registers the same channel with the same payload on every node, because every node genuinely needs to refresh its own copy.

Without node restriction, the multi-master modes break this class of work in three separate ways: leader mode would run every node's refresh against only the leader's copy, silently leaving every other node's copy stale; cluster-wide deduplication would let node A's replicated pending row suppress node B's own notify() call (so B's work would simply never be queued, with no error to warn you); and adoption would re-home a dead node B's rows onto node A, where they would be no-ops that consume scheduling state B's own logic depended on.

Declaring a channel node-restricted

SELECT pgrelay.register('refresh_view', 'SELECT app.refresh_view($1)',
    p_node_restricted := true);
-- or on an existing channel:
SELECT pgrelay.update('refresh_view', p_node_restricted := true);

What pg_relay then guarantees

  1. Every queue row for the channel is stamped with the creating node's pg_relay.node_id (in the queue.run_in_node column) by a trigger that fires before the row is inserted — covering notify(), retry rows, and companion applications that insert into pgrelay.queue directly. This stamp is always worked out by pg_relay itself, never supplied by the caller, and the trigger only fires on the node of origin, so a row arriving through replication keeps its original pin rather than being re-stamped. notify() additionally stamps the pin explicitly in its own insert, so a companion's replication-aware trigger that calls notify() during logical replication (where the origin-only trigger above is disabled) still produces a correctly pinned row.
  2. Only that one node's Processor ever dispatches the row — in every --mode, regardless of leadership or ownership. In leader mode, this is exactly why non-leaders no longer sit idle (see How Each Mode Works): each keeps draining its own pinned rows, while the leader handles everything unrestricted.
  3. The row is never adopted or reassigned (see Deciding If a Node Is Dead).
  4. p_deduplicate := true only suppresses against pending rows on the same node — two different nodes may each hold one pending row for the same (channel, payload) pair at once, which is exactly the behaviour node-local work needs. Unrestricted channels keep their usual cluster-wide deduplication.
  5. Retries inherit the pin: the retry row is inserted by whichever node ran the failed attempt, and the trigger re-stamps it there.
  6. concurrency_mode on a restricted channel serialises per node — at most one in-flight event per (channel[, payload]) combination, per node, drained oldest-first within that node. Since the work is node-local anyway, per-node scope is the correct one.

Restriction is a property of the channel, not of an individual event. If a channel's events are sometimes node-local and sometimes genuinely global, split it into two separate channels instead.

Decommissioned nodes (permanent death)

A pinned row on a node that never comes back will never run — and it will never expire either, since expiry is only checked at dispatch time, and nothing ever dispatches it. Adoption deliberately refuses to touch it: pg_relay has no way to tell "decommissioned" apart from "a long outage", and guessing wrong means either lost work or running it on the wrong node. These rows surface through queue_stats() (its pending_restricted and oldest_pending columns — see Watching Your Cluster); cleaning them up is a documented manual step:

-- Node <N> is permanently decommissioned. Its node-restricted rows can never
-- run; discard them. Run on any node (replicates).
DELETE FROM pgrelay.queue
WHERE dispatched_at IS NULL
  AND run_in_node = <N>;

This is the final chapter of the Multi-Master Deployment book. Return to the User Guide, or see the Technical Guide for everything that applies whether you run one database or many.

Copyright © 2026 Pebble IT Solutions Pty Ltd, Australia. Licensed under the MIT Licence.