Skip to main content

Channel publication filtering

Publication filtering allows clients to subscribe to a channel with a filter, ensuring that only publications with tags matching the specified criteria are delivered to the subscriber. This feature can significantly reduce bandwidth usage and minimize client-side processing overhead by filtering out irrelevant messages at the server level.

publication filtering

Optimization feature

Publication filtering is designed purely for bandwidth and performance optimization. It is not a security feature and should not be used for access control or data protection. Channel-level security and permissions should be managed through Centrifugo's authentication and authorization mechanisms. Channel subscribers can read all the data in a channel!

When combined with channels that publish messages with data tags, publication filtering enables fine-grained content delivery based on subscriber interests and requirements.

Implementation notes

  • Publication filtering works only with client-side subscriptions at this point.
  • Publication filtering is only supported by centrifuge-js for now, see below the examples
  • Publication filtering cannot be used together with delta compression in the same channel – both features serve as alternative approaches for bandwidth optimization and are mutually exclusive in Centrifugo.
  • It is recommended to avoid the design where a single subscriber to channel can not keep up with all the messages in the channel if filter is not used. Filter should be used as a bandwidth optimization, mostly in scenarios where client already skips some messages received from the channel. Or at least make sure that you don't have scenarios in the app where a subscriber is overwhelmed with messages from the channel – this results into bad UX and disconnections with slow reason. Remember – Centrifugo is a client-facing PUB/SUB system, where each channel publication is processed by each subscriber. It's a pattern completely different from "Queue" where large volume of messages in topic may be shared over many consumers thus each consumer only processes a fraction of messages achieving high throughput in terms of a single topic.
  • Publication filtering works seamlessly with Centrifugo's automatic recovery mechanisms: in case of successful recovery, only publications matching the filter are returned during stream recovery, and only the latest matching publication is returned when using cache recovery mode.
  • Centrifugo tag filters designed to be zero-allocation during publication broadcast towards many subscribers, the CPU overhead of using filter must be negligible for most setups. Having filters adds memory overhead for each subscription since Centrifugo need to keep them during the entire lifetime of the connection.
  • See more details about the decisions made in the Publication filtering by tags - reducing bandwidth with server-side stream filtering blog post.

Enable publication filtering

To allow clients the usage of publication tags-based filtering in a channel, you need to enable the feature in your Centrifugo configuration by setting the allow_tags_filter option to true for the desired namespace.

Example configuration:

config.json
{
"channel": {
"namespaces": [
{
"name": "market",
"allow_tags_filter": true
}
]
}
}

How it works

Publication filtering is based on tags – a map[string]string attached to each publication. When publishing a message, you can include tags as metadata, and subscribers can specify filters to receive only publications with tags that match their criteria.

The filtering system uses a tree-based filter structure that supports:

  • Comparison operations: equality, inequality, existence checks, string operations, numeric comparisons
  • Logical operations: AND, OR, NOT combinations
  • Set operations: membership checks

Filters are defined using a tree structure where each node can be either a comparison operation (leaf node) or a logical operation (branch node). The filter is passed to the subscription request and evaluated server-side for each publication.

FilterNode structure

Tags filter may be represented in the client protocol using FilterNode object. It may be set in client-side subscribe request. Here is its structure:

Field nameField typeDescription
opstringOperation type: skip for leaf node (comparison), "and" for logical AND, "or" for logical OR, "not" for logical NOT
keystringKey for comparison (required for leaf nodes, not used for logical operations)
cmpstringComparison operator for leaf nodes (required when op is empty). See comparison operators table below
valstringSingle value used in most comparisons (e.g. eq, neq, gt, etc.)
valsarray[string]Multiple values used for set comparisons (in, nin)
nodesarray[FilterNode]Child nodes, only for logical operations (and, or, not)

While it may seem complex, below you will see many examples which should make things crystal clear.

Comparison operators

Here's how different filter operators work:

OperatorDescriptionNotes
eqEqual to value
neqNot equal to value
inValue is in listUses vals array field
ninValue is not in listUses vals array field
exKey existsNo val or vals field needed
nexKey does not existNo val or vals field needed
swString starts with
ewString ends with
ctString contains
gtNumerically greater thanTag value must be numeric, otherwise value is skipped
gteNumerically greater than or equalTag value must be numeric, otherwise value is skipped
ltNumerically less thanTag value must be numeric, otherwise value is skipped
lteNumerically less than or equalTag value must be numeric, otherwise value is skipped

Let's say we have a publication with these tags:

{
"ticker": "AAPL",
"source": "NASDAQ",
"price": "150.25",
"category": "tech",
"volume": "1000"
}

Filter structure examples (all match the example tags above):

// Equal: ticker = "AAPL" → ✅ Matches (ticker is "AAPL")
{"key": "ticker", "cmp": "eq", "val": "AAPL"}

// Not equal: source != "TEST" → ✅ Matches (source is "NASDAQ", not "TEST")
{"key": "source", "cmp": "neq", "val": "TEST"}

// In list: category in ["tech", "finance"] → ✅ Matches (category is "tech")
{"key": "category", "cmp": "in", "vals": ["tech", "finance"]}

// Not in list: ticker not in ["MSFT", "GOOGL"] → ✅ Matches (ticker "AAPL" not in list)
{"key": "ticker", "cmp": "nin", "vals": ["MSFT", "GOOGL"]}

// Key exists: price exists → ✅ Matches (price field exists)
{"key": "price", "cmp": "ex"}

// Key does not exist: internal_id does not exist → ✅ Matches (no internal_id field)
{"key": "internal_id", "cmp": "nex"}

// String starts with: ticker starts with "AA" → ✅ Matches ("AAPL" starts with "AA")
{"key": "ticker", "cmp": "sw", "val": "AA"}

// String ends with: source ends with "DAQ" → ✅ Matches ("NASDAQ" ends with "DAQ")
{"key": "source", "cmp": "ew", "val": "DAQ"}

// String contains: category contains "ec" → ✅ Matches ("tech" contains "ec")
{"key": "category", "cmp": "ct", "val": "ec"}

// Greater than: price > 100 → ✅ Matches (150.25 > 100)
{"key": "price", "cmp": "gt", "val": "100"}

// Greater than or equal: volume >= 1000 → ✅ Matches (1000 >= 1000)
{"key": "volume", "cmp": "gte", "val": "1000"}

// Less than: price < 200 → ✅ Matches (150.25 < 200)
{"key": "price", "cmp": "lt", "val": "200"}

// Less than or equal: volume <= 1000 → ✅ Matches (1000 <= 1000)
{"key": "volume", "cmp": "lte", "val": "1000"}

Logical operators

OperatorDescription
andAll child conditions (in nodes) must be true
orAt least one child condition (in nodes) must be true
notInverts the result of a single child condition (only one node in nodes expected)

Let's say we have a publication with these tags:

{
"ticker": "AAPL",
"source": "NASDAQ",
"price": "150.25",
"category": "tech",
"volume": "1000"
}

Logical filter structure examples (all match the example tags above):

// ticker = "AAPL" AND category = "tech" → ✅ Matches (both conditions true)
{
"op": "and",
"nodes": [
{"key": "ticker", "cmp": "eq", "val": "AAPL"},
{"key": "category", "cmp": "eq", "val": "tech"}
]
}

// ticker = "MSFT" OR category = "tech" → ✅ Matches (category = "tech" is true)
{
"op": "or",
"nodes": [
{"key": "ticker", "cmp": "eq", "val": "MSFT"},
{"key": "category", "cmp": "eq", "val": "tech"}
]
}

// NOT (source = "NYSE") → ✅ Matches (source is "NASDAQ", not "NYSE")
{
"op": "not",
"nodes": [
{"key": "source", "cmp": "eq", "val": "NYSE"}
]
}

Validation and error handling

Filters are validated when a subscription is established. Invalid filters result in subscription rejection with ErrorBadRequest. Common validation issues include:

  • Missing comparison operator for leaf nodes
  • Missing key for comparison operations (except existence checks)
  • Empty value lists for in/nin operations
  • Invalid operator or comparison values
  • Incorrect child node counts for logical operations

During runtime evaluation, invalid numeric values for numeric comparisons cause the filter to evaluate to false, ensuring graceful degradation.

Publishing with tags

When publishing messages to channels (assuming Centrifugo runs on localhost:8000), include tags as part of server API publish request:

curl --header "X-API-Key: <API_KEY>" \
--request POST \
--data '{
"channel": "market:stocks",
"data": {
"ticker": "AAPL",
"source": "NASDAQ",
"price": "150.25",
"category": "tech"
},
"tags": {
"ticker": "AAPL",
"source": "NASDAQ",
"price": "150.25"
}
}' \
http://localhost:8000/api/publish

Note, tags are available on the client-side in incoming publication context – so it's not necessary to duplicate the same keys and values in both data and tags. The design to choose here is up to the application developers.

Usage in real-time SDK

Now let's see how tags filters may be set in real-time SDK. Don't forget that usage of filters must be explicitly enabled in server configuration for a namespace.

info

At this moment only centrifuge-js SDK supports tags filter.

Basic tags filter

Subscribe to a channel and receive only publications for a specific ticker:

const tagsFilter = {
key: "ticker",
cmp: "eq",
val: "AAPL"
};

const sub = centrifuge.newSubscription("market:stocks", {
tagsFilter: tagsFilter
});

More complex tags filter

Use logical operators to create more sophisticated filters (using op field):

// Receive AAPL stocks from NASDAQ only
const tagsFilter = {
op: "and",
nodes: [
{
key: "ticker",
cmp: "eq",
val: "AAPL"
},
{
key: "source",
cmp: "eq",
val: "NASDAQ"
}
]
};

const sub = centrifuge.newSubscription("market:stocks", {
tagsFilter: tagsFilter
});

Filter construction helper

For better type safety and code maintainability, consider using a filter construction helper (it's not part of centrifuge-js at this point):

const Filter = {
// Comparison operators.
eq: (key, val) => ({ key, cmp: "eq", val }),
neq: (key, val) => ({ key, cmp: "neq", val }),
in: (key, vals) => ({ key, cmp: "in", vals }),
nin: (key, vals) => ({ key, cmp: "nin", vals }),
exists: (key) => ({ key, cmp: "ex" }),
notExists: (key) => ({ key, cmp: "nex" }),
startsWith: (key, val) => ({ key, cmp: "sw", val }),
endsWith: (key, val) => ({ key, cmp: "ew", val }),
contains: (key, val) => ({ key, cmp: "ct", val }),
gt: (key, val) => ({ key, cmp: "gt", val }),
gte: (key, val) => ({ key, cmp: "gte", val }),
lt: (key, val) => ({ key, cmp: "lt", val }),
lte: (key, val) => ({ key, cmp: "lte", val }),
// Logical operators.
and: (...nodes) => ({ op: "and", nodes }),
or: (...nodes) => ({ op: "or", nodes }),
not: (node) => ({ op: "not", nodes: [node] })
};

Usage example:

// ticker = "AAPL"
const tagsFilter = Filter.eq("ticker", "AAPL");

const sub = centrifuge.newSubscription("market:stocks", {
tagsFilter: tagsFilter
});

Or more complex:

// (ticker = "AAPL") AND (price >= "100") AND (source in ["NASDAQ", "NYSE"])
const tagsFilter = Filter.and(
Filter.eq("ticker", "AAPL"),
Filter.gte("price", "100"),
Filter.in("source", ["NASDAQ", "NYSE"])
);

const sub = centrifuge.newSubscription("market:stocks", {
tagsFilter: tagsFilter
});