Skip to main content

Additional event hooks

Centrifugo PRO provides additional server-to-backend event hooks beyond the standard proxy events. These hooks notify your backend about channel-level state changes without requiring a client connection context.

Channel state events

Centrifugo PRO can send webhooks to your backend when a channel's subscriber state changes:

  • occupied — called when the first subscriber joins a channel
  • vacated — called when the last subscriber leaves a channel
Preview state

This feature is in the preview state now. We still need some time before it will be ready for usage in production. But the feature is available for evaluation.

To enable the feature you must use the redis engine. Also, only channels with presence enabled may deliver channel state notifications. When enabling the channel state proxy, Centrifugo PRO starts using another approach to create Redis keys for presence for namespaces where channel state events are enabled; this is an important implementation detail.

caution

When using client-side Redis sharding (multiple Redis shard addresses), changing the number of shards while the system has active state will result in temporary event loss. Some partitions will be routed to different shards after the change, but their data (presence entries, pending vacated events, event streams) remains on the old shards. This leads to missed vacated events and orphaned state. The system will recover as clients reconnect and re-establish presence, but channels where all clients have already disconnected will never receive a vacated event. If this is acceptable, the change can be made while the system operates. Otherwise, consider using Redis Cluster instead — it handles slot migration transparently and is fully compatible with this feature.

Configuration

Minimal config — occupied and vacated events for channels in chat namespace will be sent to the configured endpoint:

{
"engine": {
"type": "redis"
},
"channel": {
"proxy": {
"state": {
"endpoint": "http://localhost:3000/centrifugo/channel_events"
}
},
"namespaces": [
{
"name": "chat",
"presence": true,
"state_proxy_enabled": true
}
]
}
}

The proxy endpoint is an extension of Centrifugo OSS proxy and supports both HTTP and GRPC transports. For GRPC, use the grpc:// prefix in the endpoint URL. Proto definitions may be found in the proxy.proto file - see NotifyChannelState rpc. Example of the payload your backend HTTP request handler will receive:

{
"events": [
{"channel": "chat:index", "type": "occupied", "time_ms": 1697206286533},
]
}

The payload may contain a batch of events, that's why events is an array – this is important for achieving high event throughput. Your backend must be fast enough to keep up with the events rate and volume, otherwise event queues will grow and eventually new events will be dropped by Centrifugo PRO.

Respond with an empty result object, without an error object set, to let Centrifugo PRO know that events were processed successfully. If the request to the backend fails or the response contains an error object, Centrifugo PRO will retry sending events with exponential backoff (from 100ms up to 20s).

Here is an example of an HTTP handler for processing channel state events using Flask:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/centrifugo/channel_events', methods=['POST'])
def channel_events():
body = request.get_json()
for event in body.get('events', []):
channel = event['channel']
event_type = event['type']
time_ms = event['time_ms']
if event_type == 'occupied':
print(f'Channel {channel} occupied at {time_ms}')
# First subscriber joined the channel - allocate resources, etc.
elif event_type == 'vacated':
print(f'Channel {channel} vacated at {time_ms}')
# Last subscriber left the channel - clean up resources, etc.
return jsonify({'result': {}})

if __name__ == '__main__':
app.run(port=3000)

Vacated event delay

When the last subscriber leaves a channel, Centrifugo PRO delays the vacated event by a configurable interval (default 5s) before sending it. If a client resubscribes during this interval, the vacated event is cancelled. This avoids unnecessary webhooks for quick reconnect scenarios. These are configurable via channel_state options. num_partitions (default 128) sets the number of isolated partitions used to serialize channel state events in the system.

caution

num_partitions must not be changed after the system is already operating with active channels. Changing it alters the channel-to-partition mapping, which means existing state in Redis (presence data, pending vacated events, event streams) becomes orphaned on old partitions. This will result in missed vacated events for currently occupied channels and possible spurious occupied/vacated pairs as clients reconnect. The system will recover as clients reconnect and rebuild presence state, but channels where all clients have already disconnected will never receive a vacated event. If this is acceptable, the change can be made while the system operates. Otherwise, plan the value before going to production and keep it fixed.

config.json
{
"engine": {
"type": "redis",
"redis": {
"channel_state": {
"vacated_event_delay": "10s",
"num_partitions": 128
}
}
}
}
caution

Redis used for channel state events should be configured with maxmemory-policy noeviction (or a volatile-* policy). The feature relies on several Redis keys without TTL (event streams, pending vacated queues, expiration tracking sets). If Redis evicts these keys under memory pressure, events will be permanently lost — occupied channels may never receive vacated events. Consider using a separate presence manager with a dedicated Redis instance for namespaces with channel state events enabled — this isolates memory usage from the main engine Redis and gives you full control over eviction policy.

For example, to use a dedicated Redis instance for presence with channel state events:

config.json
{
"engine": {
"type": "redis"
},
"presence_manager": {
"enabled": true,
"type": "redis",
"redis": {
"address": "localhost:6380"
}
},
"channel": {
"proxy": {
"state": {
"endpoint": "http://localhost:3000/centrifugo/channel_events"
}
},
"namespaces": [
{
"name": "chat",
"presence": true,
"state_proxy_enabled": true
}
]
}
}

Centrifugo PRO does the best effort delivering channel state events, making retries when the backend endpoint is unavailable (with exponential backoff), also survives cases when Centrifugo node dies unexpectedly. But there are scenarios when events may be lost — some of them are described above (Redis eviction, configuration changes). Even as best-effort notifications, channel state events can be very useful for applications — for example, to lazily clean up resources or update external state when channels become empty. For cases where stronger consistency is required, we recommend periodically syncing state by querying channel presence information using the server API.

Cache empty events

Centrifugo PRO can notify the backend when a client subscribes to a channel using cache recovery mode, but there is no latest publication found in the history stream to load the initial state – i.e. in the case of "cache miss" event. The backend may react to the event and populate the cache by publishing the current state to the channel.

This is done by configuring "cache empty" proxy. It's similar to proxies described in Proxy events to the backend chapter, but acts without client connection context – because it's related to a channel in general, and a particular client who triggered the cache miss is not important.

Configuration

Add the following options to the configuration file:

config.json
{
"channel": {
"proxy": {
"cache_empty": {
"endpoint": "http://localhost:3000/centrifugo/cache_empty",
"timeout": "1s"
}
}
}
}

To enable the proxy for desired channels, use the cache_empty_proxy_enabled channel namespace option. For example, to enable it for channels without namespace:

config.json
{
"channel": {
"proxy": {
"cache_empty": {
"endpoint": "http://localhost:3000/centrifugo/cache_empty",
"timeout": "1s"
}
},
"without_namespace": {
"cache_empty_proxy_enabled": true
}
}
}

Or for a specific namespace:

config.json
{
"channel": {
"proxy": {
"cache_empty": {
"endpoint": "http://localhost:3000/centrifugo/cache_empty",
"timeout": "1s"
}
},
"namespaces": [{
"name": "example",
"cache_empty_proxy_enabled": true
}]
}
}

Request and response

Payload example sent to app backend in cache empty notification request:

{
"channel": "example:index"
}

Expected response example:

{
"result": {}
}

If cache empty proxy is defined, but Centrifugo can't reach it – then subscription request which triggered the event will be rejected with the internal error.

CacheEmptyRequest

FieldTypeRequiredDescription
channelstringyesA channel in which cache miss occurred

CacheEmptyResult

FieldTypeRequiredDescription
populatedbooleannoNotify Centrifugo that channel cache was populated by the app backend – in this case Centrifugo will try to recover state one more time