Built-in API command async consumers
In server API chapter we've shown how to execute various Centrifugo server API commands (publish, broadcast, etc.) over HTTP or GRPC. In many cases you will call those APIs from your application business logic synchronously. But to deal with temporary network and availability issues, and achieve reliable execution of API commands upon changes in your primary application database you may want to use queuing techniques and call Centrifugo API asynchronously.
Asynchronous delivery of real-time events upon changes in primary database may be done is several ways. Some companies use transactional outbox pattern, some using techniques like Kafka Connect with CDC (Change Data Capture) approach. The fact Centrifugo provides API allows users to implement any of those techniques and build worker which will send API commands to Centrifugo reliably.
But Centrifugo also provides some built-in asynchronous consumers to simplify the integration process.
The application writes an API command to a queue, a stream or an outbox table instead of calling Centrifugo over HTTP. Centrifugo consumes it and executes the command with the same handlers the synchronous API uses.
Supported consumersโ
The following built-in async consumers are available at this point:
- from PostgreSQL outbox table
- from Kafka topics
- from Nats Jetstream
- from Redis Streams
- from Google Cloud PUB/SUB
- from AWS SQS
- from Azure Service Bus
Again, while built-in consumers can simplify integration, you still can use whatever queue system you need and integrate your own consumer with Centrifugo sending requests to server API.
We also recommend looking at Pitfalls of async publishing part in our previous blog post โ while in many cases you get reliable at least once processing, you may come across some pitfalls in the process, being prepared and understanding them is important. Then depending on the real-time feature you can decide which approach is better โ synchronous publishing or asynchronous integration.
How consumers workโ
By default, consumers expect to consume messages which represent Centrifugo server API commands. I.e. while in synchronous server API you are using HTTP or GRPC to send commands โ with asynchronous consumers you are inserting API command to PostgreSQL outbox table, or delivering to Kafka topic โ and it will be soon consumed and processed asynchronously by Centrifugo.
Async consumers only process commands which modify state โ such as publish, broadcast, unsubscribe, disconnect, etc. Sending read commands for async execution simply does not make any sense, and they will be ignored. Also, batch method is not supported.
Centrifugo only supports JSON payloads for asynchronous commands coming to consumers for now. If you need binary format โ reach out with your use case.
If Centrifugo encounters an error while processing consumed messages โ then temporary errors (internal error 100 and, since v6.10.0, too many requests 111) will be retried, all other errors logged on error level โ and the message will be marked as processed. Errors which retrying can not fix, such as bad request 107, are not retried. The processing logic for broadcast API is special: if any of the publications to any channel from broadcast channels array failed with a temporary error โ then the entire broadcast command will be retried. To prevent duplicate messages being published during such retries โ consider using idempotency_key in the broadcast command.
Our Chat/Messenger tutorial shows PostgreSQL outbox and Kafka consumer in action. It also shows techniques to avoid duplicate messages (idempotent publications) and deal with late message delivery (idempotent processing on client side). Whether you need those techniques โ depends on the nature of app. Various real-time features may require different ways of sending real-time events. Both synchronous API calls and async calls have its own advantages and trade-offs. We also talk about this in Asynchronous message streaming to Centrifugo with Benthos blog post.
Publication data modeโ
As mentioned, Centrifugo expects server API commands in received message content. Once the command consumed โ it's processed in the same way as HTTP or GRPC server APIs process the request.
Sometimes though, you may have a system that already produces messages in a format ready to be published into Centrifugo channels. Most Centrifugo async consumers have a special mode to consume publications โ called Publication Data Mode. In that case, payload of message must contain a data ready to be published into Centrifugo channels. Users can provide Centrifugo-specific publication fields like a list of channels to publish into in message headers/attributes. See documentation of each specific consumer to figure out exact option names. For example, Kafka consumer has publication data mode. And similar for other consumers.
Note, since you can provide many consumers in Centrifugo configuration - it's totally possible to have consumers working in different modes.
Ordering guaranteesโ
Carefully read specific consumer documentation for understanding message processing ordering properties โ ordered processing can be achieved with some of them, and can not with others.
How to enableโ
Consumers can be set in the configuration using consumers array:
{
"consumers": [
{
"enabled": true,
"name": "xxx",
"type": "postgresql",
"postgresql": {...}
},
{
"enabled": true,
"name": "yyy",
"type": "kafka",
"kafka": {...}
}
]
}
consumers[]โ
So consumers may be configured using consumers array on configuration top level.
On top level each consumer object in the consumers array has the following configuration options.
consumers[].enabledโ
Boolean. Default: false.
When set to true allows enabling the configured consumer.
consumers[].nameโ
String. Default: "". Required.
Describes name of consumer. Must be unique for each consumer and match the regex ^[a-zA-Z0-9_]{2,}$ - i.e. latin symbols, digits and underscores and be at least 2 symbols. This name will be used for logging purposes, metrics, also to override some options with environment variables.
consumers[].typeโ
String. Default: "". Required.
Type of consumer. At this point can be:
postgresqlkafkanats_jetstreamredis_streamgoogle_pub_subaws_sqsazure_service_bus
Configure via env varsโ
To provide consumers over environment variable provide CENTRIFUGO_CONSUMERS var with JSON array serialized to string.
It's also possible to override consumer options over environment variables by using the name of consumer. For example:
CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_<OPTION_NAME>="???"
Or for specific type configuration:
CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_POSTGRESQL_<OPTION_NAME2>="???"
PostgreSQL outbox consumerโ
Centrifugo can natively integrate with PostgreSQL table for Transactional outbox pattern. The table in PostgreSQL must have predefined format Centrifugo expects:
CREATE TABLE IF NOT EXISTS centrifugo_outbox (
id BIGSERIAL PRIMARY KEY,
method text NOT NULL,
payload JSONB NOT NULL,
partition INTEGER NOT NULL default 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
Then configure consumer of postgresql type in Centrifugo config:
{
...
"consumers": [
{
"enabled": true,
"name": "my_postgresql_consumer",
"type": "postgresql",
"postgresql": {
"dsn": "postgresql://user:password@localhost:5432/db",
"outbox_table_name": "centrifugo_outbox",
"num_partitions": 1,
"partition_select_limit": 100,
"partition_poll_interval": "300ms"
}
}
]
}
Here is how you can insert row in outbox table to publish into Centrifugo channel:
INSERT INTO centrifugo_outbox (method, payload, partition)
VALUES ('publish', '{"channel": "updates", "data": {"text": "Hello, world!"}}', 0);
Centrifugo supports LISTEN/NOTIFY mechanism of PostgreSQL to be notified about new data in the outbox table. To enable it you need first create a trigger in PostgreSQL:
CREATE OR REPLACE FUNCTION centrifugo_notify_partition_change()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('centrifugo_partition_change', NEW.partition::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER centrifugo_notify_partition_trigger
AFTER INSERT ON centrifugo_outbox
FOR EACH ROW
EXECUTE FUNCTION centrifugo_notify_partition_change();
And then update consumer config โ add "partition_notification_channel" option to it:
{
...
"consumers": [
{
"enabled": true,
"name": "my_postgresql_consumer",
"type": "postgresql",
"postgresql": {
...
"partition_notification_channel": "centrifugo_partition_change"
}
}
]
}
consumers[].postgresqlโ
Options for consumer of postgresql type.
consumers[].postgresql.dsnโ
String. Default: "". Required.
DSN to PostgreSQL database, ex. "postgresql://user:password@localhost:5432/db". To override dsn over environment variables use CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_POSTGRESQL_DSN.
consumers[].postgresql.outbox_table_nameโ
String. Default: "". Required.
The name of outbox table in selected database, ex. "centrifugo_outbox".
consumers[].postgresql.num_partitionsโ
Integer. Default: 1.
The number of partitions to use. Centrifugo keeps strict order of commands per-partition by default. This option provides a way to create concurrent consumers each consuming from different partition of outbox table. Note, that partition numbers in start with 0, so when using 1 as num_partitions insert data with partition == 0 to the outbox table.
consumers[].postgresql.partition_select_limitโ
Integer. Default: 100.
Max number of commands to select in one query to outbox table.
consumers[].postgresql.partition_poll_intervalโ
Duration. Default: "300ms".
Polling interval for each partition.
consumers[].postgresql.partition_notification_channelโ
String. Default: "".
Optional name of LISTEN/NOTIFY channel to trigger consuming upon data added to outbox partition.
PostgreSQL LISTEN/NOTIFY requires a persistent, dedicated connection. PGBouncer in transaction pooling mode (the most common setup) is incompatible with this โ it may route the LISTEN command and subsequent notifications to different backend connections. If your dsn points at PGBouncer, set partition_notification_dsn to a direct PostgreSQL URL for the LISTEN connection. If partition_notification_dsn is not set, the primary pool is used (fine for direct PostgreSQL connections).
consumers[].postgresql.partition_notification_dsnโ
String. Default: "".
Optional separate DSN used exclusively for the LISTEN connection when partition_notification_channel is set. Use this to provide a direct PostgreSQL URL (bypassing PGBouncer) when the primary dsn points at a PGBouncer endpoint. PGBouncer transaction pooling mode is incompatible with LISTEN/NOTIFY. If empty, the primary DSN pool is used.
consumers[].postgresql.tlsโ
TLS object. By default, no TLS is used.
Client TLS configuration for PostgreSQL connection.
consumers[].postgresql.use_try_lockโ
Boolean. Default: false.
Use pg_try_advisory_xact_lock instead of pg_advisory_xact_lock for locking outbox table. This may help to reduce the number of longer-running transactions on PG side.
Kafka consumerโ
Another built-in consumer โ is Kafka topics consumer. To configure Centrifugo to consume Kafka topic:
{
"consumers": [
{
"enabled": true,
"name": "my_kafka_consumer",
"type": "kafka",
"kafka": {
"brokers": ["localhost:9092"],
"topics": ["postgres.public.chat_cdc"],
"consumer_group": "centrifugo"
}
}
]
}
Then simply put message in the following format to Kafka topic:
{
"method": "publish",
"payload": {
"channel": "mychannel",
"data": {}
}
}
โ and it will be consumed by Centrifugo and reliably processed.
Centrifugo preserves processing order within Kafka partitions.
consumers[].kafkaโ
Options for consumer of kafka type.
consumers[].kafka.brokersโ
Array of string. Required.
Points Centrifugo to Kafka brokers. To override brokers over environment variables use CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_KAFKA_BROKERS โ string with broker addresses separated by space.
consumers[].kafka.topicsโ
Array of string. Required.
Tells which topics to consume.
consumers[].kafka.consumer_groupโ
String. Required.
Sets the name of consumer group to use.
consumers[].kafka.max_poll_recordsโ
Integer. Default: 100.
Sets the maximum number of records to fetch from Kafka during a single poll operation.
consumers[].kafka.fetch_max_bytesโ
Integer. Default: 52428800 (50MB).
Sets the maximum number of bytes to fetch from Kafka in a single request. In many cases setting this to lower value can help with aggressive Kafka client memory usage under load.
consumers[].kafka.fetch_max_waitโ
Type: Duration. Default: 500ms. New in Centrifugo v6.2.3
Sets the maximum time to wait for records when polling.
consumers[].kafka.fetch_read_uncommittedโ
Boolean. Default: false. New in Centrifugo v6.2.3
If set to true, the consumer will read uncommitted messages from Kafka. By default, it uses ReadCommitted mode.
consumers[].kafka.sasl_mechanismโ
String. Default: "".
SASL mechanism to use: "plain", "scram-sha-256", "scram-sha-512", "aws-msk-iam" are supported. In case of "aws-msk-iam", by default Centrifugo uses sasl_user and sasl_password options as access key and secret key when configuring AWS auth. Alternatively, set assume_role_arn to authenticate via the AWS SDK default credential chain combined with STS AssumeRole โ useful for cross-account MSK access or when running Centrifugo on EC2/EKS/ECS with an instance profile.
consumers[].kafka.sasl_userโ
String. Default: "".
User for plain SASL auth. To override sasl_user over environment variables use CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_KAFKA_SASL_USER.
consumers[].kafka.sasl_passwordโ
String. Default: "".
Password for plain SASL auth. To override sasl_password over environment variables use CENTRIFUGO_CONSUMERS_<CONSUMER_NAME>_KAFKA_SASL_PASSWORD.
consumers[].kafka.assume_role_arnโ
String. Default: "". New in Centrifugo v6.7.1
When set together with sasl_mechanism: "aws-msk-iam", Centrifugo uses AWS STS AssumeRole to obtain temporary credentials for MSK IAM authentication. Base credentials and region are loaded via the default AWS SDK credential chain (AWS_REGION, environment variables, EC2/EKS/ECS instance metadata, shared profile, etc.), then used to assume the specified IAM role. The resulting session token is automatically refreshed by the Kafka client.
This is useful for cross-account MSK access, or when running Centrifugo with an instance profile and you want it to assume a role that has MSK permissions.
When assume_role_arn is empty, Centrifugo falls back to the static sasl_user / sasl_password keys. When assume_role_arn is set, sasl_user and sasl_password are ignored. Setting assume_role_arn with any SASL mechanism other than aws-msk-iam is a configuration error.
consumers[].kafka.tlsโ
TLSConfig to configure Kafka client TLS.
consumers[].kafka.publication_data_modeโ
Publication data mode for Kafka consumer simplifies integrating Centrifugo with existing Kafka topics. By default, Centrifugo can integrate with Kafka topics but requires a special payload format, where each message in the topic represented a Centrifugo API command. This approach works well for Kafka topics specifically set up for Centrifugo.
When Publication Data Mode is enabled, Centrifugo expects messages in Kafka topics to contain data ready for direct publication, rather than server API commands. It is also possible to use special Kafka headers to specify the channels to which the data should be published.
The primary goal of this mode is to simplify Centrifugo's integration with existing Kafka topics, making it easier to deliver real-time messages to clients without needing to restructure the topic's payload format.
BTW, don't forget that since Centrifugo allows configuring an array of asynchronous consumers, it is possible to use Kafka consumers in different modes simultaneously.
To enable publication data mode:
{
"consumers": [
{
"enabled": true,
"name": "my_kafka_consumer",
"type": "kafka",
"kafka": {
"brokers": ["localhost:9092"],
"topics": ["my_topic"],
"consumer_group": "centrifugo",
"publication_data_mode": {
"enabled": true,
"channels_header": "x-centrifugo-channels",
"idempotency_key_header": "x-centrifugo-idempotency-key"
}
}
}
]
}
As you can see, channels to forward publication to may be provided as a value of a configured header. So you don't need to change payloads in topic to transform them to real-time messages with Centrifugo.
consumers[].kafka.publication_data_mode.enabledโ
Boolean. Default: false.
Enables Kafka publication data mode for the Kafka consumer.
consumers[].kafka.publication_data_mode.channels_headerโ
String. Default: "".
Header name to extract channels to publish data into (channels must be comma-separated). Ex. of value: "channel1,channel2".
consumers[].kafka.publication_data_mode.idempotency_key_headerโ
String. Default: "".
Header name to extract Publication idempotency key from Kafka message. See PublishRequest.
consumers[].kafka.publication_data_mode.delta_headerโ
String. Default: "".
Header name to extract Publication delta flag from Kafka message which tells Centrifugo whether to use delta compression for message or not. See delta compression and PublishRequest.
Compatibility with Redpandaโ
Our local test suite for Kafka consumer passed with Redpanda v24.3.6, so it's generally compatible.
Nats Jetstreamโ
Consumer from Nats Jetstream.
Note, message processing is unordered in case you have multiple Centrifugo instances consuming from Nats Jetstream (but possible if only one instance of Centrifugo consumes).
Keep in mind, that Centrifugo does not create Nats Jetstream streams, it consumes from pre-created streams. Pay attention to a situation when Nats Jetstream streams are created in-memory or inside temporary directory of operating system. In such cases Nats streams may be unexpectedly lost at some point โ this is a common mistake when working with Nats Jetstream.
consumers[].nats_jetstreamโ
Type: NatsJetStreamConsumerConfig object
nats_jetstream allows defining options for consumer of nats_jetstream type.
consumers[].nats_jetstream.urlโ
Type: string. Default: nats://127.0.0.1:4222
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_URL
url is the address of the NATS server.
consumers[].nats_jetstream.credentials_fileโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_CREDENTIALS_FILE
credentials_file is the path to a NATS credentials file used for authentication (nats.UserCredentials).
If provided, it overrides username/password and token.
consumers[].nats_jetstream.usernameโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_USERNAME
username is used for basic authentication (along with Password) if CredentialsFile is not provided.
consumers[].nats_jetstream.passwordโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_PASSWORD
password is used with Username for basic authentication.
consumers[].nats_jetstream.tokenโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_TOKEN
token is an alternative authentication mechanism if CredentialsFile and Username are not provided.
consumers[].nats_jetstream.stream_nameโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_STREAM_NAME
stream_name is the name of the NATS JetStream stream to use.
consumers[].nats_jetstream.subjectsโ
Type: []string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_SUBJECTS
subjects is the list of NATS subjects (topics) to filter.
consumers[].nats_jetstream.durable_consumer_nameโ
Type: string
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_DURABLE_CONSUMER_NAME
durable_consumer_name sets the name of the durable JetStream consumer to use.
consumers[].nats_jetstream.deliver_policyโ
Type: string. Default: new
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_DELIVER_POLICY
deliver_policy is the NATS JetStream delivery policy for the consumer. By default, it is set to "new". Possible values: new, all.
consumers[].nats_jetstream.max_ack_pendingโ
Type: int. Default: 100
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_MAX_ACK_PENDING
max_ack_pending is the maximum number of unacknowledged messages that can be pending for the consumer.
consumers[].nats_jetstream.method_headerโ
Type: string. Default: centrifugo-method
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_METHOD_HEADER
method_header is the NATS message header used to extract the method name for dispatching commands.
If provided in message, then payload must be just a serialized API request object.
consumers[].nats_jetstream.use_existing_consumerโ
Type: bool. Default: false
New in Centrifugo v6.5.2
use_existing_consumer when enabled tells Centrifugo to use an existing consumer with durable_consumer_name instead of creating a new one. When on, these fields are ignored: deliver_policy, subjects, max_ack_pending, and all other consumer-creation-related options which may be added later (like ack wait, etc.). The existing consumer's configuration defines all behavior, and Centrifugo will fail to start if the consumer does not already exist.
consumers[].nats_jetstream.publication_data_modeโ
Type: NatsJetStreamPublicationDataModeConfig object
publication_data_mode configures extraction of pre-formatted publication data from message headers.
consumers[].nats_jetstream.publication_data_mode.enabledโ
Type: bool
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_PUBLICATION_DATA_MODE_ENABLED
enabled toggles publication data mode.
consumers[].nats_jetstream.publication_data_mode.channels_headerโ
Type: string. Default: centrifugo-channels
Env: CENTRIFUGO_CONSUMERS_<NAME>_NATS_JETSTREAM_PUBLICATION_DATA_MODE_CHANNELS_HEADER
channels_header is the name of the header that contains comma-separated channel names.