Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sockudo/sockudo/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Tag filtering allows clients to subscribe to channels with server-side filters, so they only receive messages matching specific criteria. This dramatically reduces bandwidth usage and processing overhead, particularly beneficial for mobile devices and high-volume channels. Bandwidth Savings: 60-90% in high-volume scenarios
Tag filtering is disabled by default for Pusher backward compatibility. You must explicitly enable it via configuration.

Quick Start

1. Enable Tag Filtering

Environment Variable:
TAG_FILTERING_ENABLED=true
Configuration File:
config/config.json
{
  "tag_filtering": {
    "enabled": true
  }
}

2. Add Tags to Publications (Server)

curl -X POST http://localhost:6001/apps/my-app/events \
  -H "Content-Type: application/json" \
  -d '{
    "name": "match_event",
    "channel": "match:123",
    "data": "{\"minute\":\"23\",\"player\":\"Mbappe\"}",
    "tags": {
      "event_type": "goal",
      "team": "Real Madrid"
    }
  }'

3. Subscribe with Filter (Client)

import Pusher, { Filter } from 'sockudo-js';

const pusher = new Pusher('my-app-key', {
  wsHost: 'localhost',
  wsPort: 6001
});

// Only receive goal events
const channel = pusher.subscribe('match:123', Filter.eq('event_type', 'goal'));

channel.bind('match_event', (data) => {
  console.log('Goal!', data);
  // This will ONLY fire for goals, not shots, passes, etc.
});

How It Works

Architecture

  1. Tags on Publications: Each publication can have optional tags (key-value string pairs)
  2. Filters on Subscriptions: Clients specify filters when subscribing to channels
  3. Evaluation During Broadcast: Filters are evaluated with zero allocations during message broadcast
  4. Filtered Delivery: Only messages matching the filter are sent to that subscriber

Performance Characteristics

  • Compilation: Filter validation happens once at subscription time (~100-200ns)
  • Evaluation: Single filter evaluation takes ~10-95ns depending on complexity
  • Memory: Zero allocations per evaluation (hot path optimization)
  • Scale: 10k subscribers evaluated in ~100-900μs depending on filter complexity

Filter Operators

Comparison Operators

OperatorDescriptionExample
eqEqualityFilter.eq('event_type', 'goal')
neqInequalityFilter.neq('event_type', 'pass')
inSet membershipFilter.in('event_type', ['goal', 'shot'])
ninSet non-membershipFilter.nin('event_type', ['pass', 'tackle'])
gtGreater than (numeric)Filter.gt('xG', '0.8')
gteGreater than or equalFilter.gte('price', '100.00')
ltLess than (numeric)Filter.lt('count', '10')
lteLess than or equalFilter.lte('score', '3')

String Operations

OperatorDescriptionExample
startsWithString starts withFilter.startsWith('ticker', 'GOOG')
endsWithString ends withFilter.endsWith('ticker', 'L')
containsString containsFilter.contains('description', 'important')

Existence Checks

OperatorDescriptionExample
existsKey exists in tagsFilter.exists('xG')
notExistsKey does not existFilter.notExists('penalty')

Logical Operators

OperatorDescriptionExample
andAll must matchFilter.and(Filter.eq('type', 'shot'), Filter.gte('xG', '0.8'))
orAt least one must matchFilter.or(Filter.eq('type', 'goal'), Filter.eq('type', 'penalty'))
notNegates filterFilter.not(Filter.eq('type', 'pass'))

Filter Examples

Simple Filters

// Single equality
Filter.eq('event_type', 'goal')

// Multiple event types
Filter.in('event_type', ['goal', 'penalty', 'red_card'])

// Numeric threshold
Filter.gte('price', '100.00')

// String pattern
Filter.startsWith('ticker', 'GOOG')

Complex Filters

Football Match Events - Goals OR high-probability shots:
const filter = Filter.or(
  Filter.eq('event_type', 'goal'),
  Filter.and(
    Filter.eq('event_type', 'shot'),
    Filter.gte('xG', '0.8')
  )
);

const channel = pusher.subscribe('match:123', filter);
Stock Market Tickers - Specific tickers with price threshold:
const filter = Filter.and(
  Filter.in('ticker', ['GOOGL', 'AAPL', 'MSFT']),
  Filter.gte('price', '100.00')
);

const channel = pusher.subscribe('stock-prices', filter);
IoT Sensor Data - Critical sensor readings:
const filter = Filter.or(
  Filter.and(
    Filter.eq('sensor_type', 'temperature'),
    Filter.gt('value', '100')
  ),
  Filter.and(
    Filter.eq('sensor_type', 'pressure'),
    Filter.lt('value', '20')
  )
);

const channel = pusher.subscribe('iot-sensors', filter);

Use Cases

1. Sports Live Scores

Problem: Mobile apps don’t need every pass/tackle, just important events Solution:
// Client only interested in goals and red cards
const filter = Filter.in('event_type', ['goal', 'red_card']);
const channel = pusher.subscribe('match:live', filter);
Result: 90% bandwidth reduction on high-activity matches

2. Stock Market Real-Time Data

Problem: Trading apps track 10-20 stocks from thousands available Solution:
// Only track watchlist stocks
const filter = Filter.and(
  Filter.in('ticker', watchlist),
  Filter.gte('price', '50.00')
);
const channel = pusher.subscribe('stocks', filter);
Result: 95% bandwidth reduction, lower data costs

3. IoT Monitoring

Problem: Dashboard only needs critical alerts, not all sensor readings Solution:
// Only critical/high severity
const filter = Filter.or(
  Filter.eq('severity', 'critical'),
  Filter.eq('severity', 'high')
);
const channel = pusher.subscribe('alerts', filter);
Result: 80% bandwidth reduction, reduced alert fatigue

4. Gaming Events

Problem: Players only care about rare drops and personal achievements Solution:
// High-value drops and achievements
const filter = Filter.or(
  Filter.gte('item_rarity', '4'),
  Filter.eq('type', 'achievement')
);
const channel = pusher.subscribe('game-events', filter);
Result: 70% bandwidth reduction, better UX

5. Social Media Feeds

Problem: Users follow specific topics/hashtags, not all content Solution:
// Filter by hashtag
const filter = Filter.contains('hashtags', 'breaking');
const channel = pusher.subscribe('feed', filter);
Result: 85% bandwidth reduction, relevant content only

Performance Benchmarks

Simple Filter: event_type == "goal"

OperationTimeAllocations
Compilation (subscription)103.5ns3 allocs (328 bytes)
Single evaluation11.69ns0 allocs
10k evaluations (broadcast)112.08μs0 allocs

Complex Filter: (event_type == "shot" AND xG >= "0.8") AND (count > 42 OR price >= 99.5)

OperationTimeAllocations
Compilation (subscription)206.8ns5 allocs (664 bytes)
Single evaluation93.78ns0 allocs
10k evaluations (broadcast)923.83μs0 allocs

Bandwidth Savings

ScenarioWithout FilteringWith FilteringSavings
High-volume sports channel1000 msg/sec50-100 msg/sec90%
Stock market data10,000 msg/sec200-500 msg/sec95%
IoT sensor alerts500 msg/sec50-100 msg/sec80%

Best Practices

1. Design Your Tag Schema

Plan your tag structure based on how clients will filter:
// Good: Filterable tags
tags: {
  "event_type": "goal",
  "team": "Real Madrid",
  "player_id": "9",
  "minute": "23"
}

// Bad: Nested or complex structures
tags: {
  "metadata": "{\"event\":\"goal\",\"team\":\"Real Madrid\"}"
}

2. Keep Filters Simple

Simpler filters evaluate faster:
// Good: Simple filter
Filter.eq('event_type', 'goal')

// OK: Moderate complexity
Filter.or(
  Filter.eq('event_type', 'goal'),
  Filter.eq('event_type', 'penalty')
)

// Avoid: Overly complex nested filters
Filter.and(
  Filter.or(...many nodes...),
  Filter.or(...many nodes...),
  Filter.not(Filter.and(...many nodes...))
)

3. Use Set Operations Efficiently

For multiple values, use in instead of multiple or conditions:
// Good
Filter.in('event_type', ['goal', 'shot', 'penalty'])

// Less efficient
Filter.or(
  Filter.eq('event_type', 'goal'),
  Filter.eq('event_type', 'shot'),
  Filter.eq('event_type', 'penalty')
)

4. Validate Filters Client-Side

Catch filter errors early:
import { validateFilter } from 'sockudo-js';

const filter = buildFilterFromUserInput();
const error = validateFilter(filter);
if (error) {
  showErrorToUser(error);
  return;
}
pusher.subscribe(channelName, filter);

5. Consider Message Volume

Filters are most beneficial when:
  • Channel has high message volume (>100 messages/second)
  • Clients need only a small subset (<20% of messages)
  • Many concurrent subscribers with different filter needs

Combining with Delta Compression

Tag filtering and delta compression work seamlessly together for maximum efficiency:
// Enable delta compression + tag filtering
pusher.connection.bind('connected', () => {
  pusher.connection.send_event('pusher:enable_delta_compression', {});
});

const channel = pusher.subscribe('market-data', {
  filter: Filter.in('ticker', ['GOOGL', 'AAPL', 'MSFT']),
  delta: { enabled: true, algorithm: 'Fossil' }
});
Result: 95%+ total bandwidth reduction (filtering + compression)

Limitations

  1. Tags must exist on publication: Messages without tags will not match any filter (even existence checks)
  2. String-based values: All tag values are strings (numeric comparisons parse strings as needed)
  3. No regex support: Use startsWith, endsWith, contains for pattern matching
  4. Filter size: Keep filters reasonably sized (recommended <50 nodes)
  5. No security boundary: Filters don’t add permission checks, they only reduce bandwidth

Dynamic Filter Updates

To change filters, unsubscribe and resubscribe:
// Initial subscription with one filter
let channel = pusher.subscribe('match:123', Filter.eq('event_type', 'goal'));

// Later, change filter
pusher.unsubscribe('match:123');
channel = pusher.subscribe('match:123', Filter.in('event_type', ['goal', 'shot']));

Troubleshooting

Messages Not Being Received

Check tags exist: Ensure publications have the tags you’re filtering on
# Verify tags in publication
curl -X POST http://localhost:6001/apps/my-app/events \
  -d '{
    "name": "event",
    "channel": "test",
    "data": "{}",
    "tags": {"type": "test"}  # Tags present
  }'
Validate filter: Use validateFilter() to check for syntax errors
import { validateFilter } from 'sockudo-js';

const filter = Filter.eq('event_type', 'goal');
const error = validateFilter(filter);
if (error) {
  console.error('Invalid filter:', error);
}
Test without filter: Verify messages arrive without filter first
// Test without filter
const channel = pusher.subscribe('match:123');
channel.bind('match_event', (data) => {
  console.log('Received:', data);
});

Filter Not Working as Expected

Case sensitivity: All comparisons are case-sensitive
// These are different
Filter.eq('type', 'Goal')  // Won't match 'goal'
Filter.eq('type', 'goal')  // Correct
String vs numeric: Numeric operators parse strings
// Both work (values are strings in tags)
Filter.gt('count', '10')
Filter.gt('count', 10)  // Converted to string
Missing tags: Messages without tags won’t match any filter

Performance Issues

Simplify filters: Reduce nesting depth and number of nodes Use appropriate operators: in for sets, not multiple or conditions Check tag count: Fewer tags per message = faster evaluation

Migration Guide

Adding Filters to Existing Application

1. Enable tag filtering (backward compatible - old clients ignore tags)
TAG_FILTERING_ENABLED=true
2. Add tags to publications (existing clients continue to work)
# Start adding tags to new publications
curl -X POST http://localhost:6001/apps/my-app/events \
  -d '{
    "tags": {"event_type": "goal"}
  }'
3. Update clients to use new filter API when subscribing
// New clients with filtering
const channel = pusher.subscribe('match:123', Filter.eq('event_type', 'goal'));

// Old clients without filtering (still work)
const channel = pusher.subscribe('match:123');
4. Monitor bandwidth to measure savings 5. Gradually roll out filters to different client segments

Next Steps

Delta Compression

Combine with delta compression for 95%+ bandwidth reduction

Rate Limiting

Control message throughput per application