ChatClient.Builder(apiKey, context)
.config(
ChatClientConfig(
messageLimitConfig = MessageLimitConfig(
channelMessageLimits = setOf(
// Limit livestream channels to 500 messages in memory
ChannelMessageLimit(channelType = "livestream", baseLimit = 500),
// Limit messaging channels to 1000 messages in memory
ChannelMessageLimit(channelType = "messaging", baseLimit = 1000),
),
),
)
)
.build()Livestream Chat
High-traffic channels like livestreams can generate thousands of messages per minute. The SDK provides configuration options to optimize memory usage and performance for these scenarios.
Message Limiting
The MessageLimitConfig allows you to control how many messages are kept in memory for different channel types. When the message count exceeds the configured limit, older messages are automatically trimmed from memory while keeping the most recent ones.
Configuration
Configure message limits when setting up the client:
How It Works
- When the message count exceeds
baseLimit + 30(buffer), the SDK trims messages down tobaseLimit - Messages are sorted by creation time; only the most recent ones are kept
- Trimming only occurs when not loading older messages (to avoid interfering with pagination)
- If loading older messages causes the count to exceed the limit, the limit is temporarily increased by 1.5x
- The
endOfOlderMessagesflag is set tofalsewhen trimming occurs, indicating more messages are available but not yet loaded in memory - Messages are only removed from in-memory state, not from the local database
UI Integration
If you're using Jetpack Compose, the MessageList component works seamlessly with message limiting. Compose's LazyColumn only renders visible items, providing efficient virtualization automatically:
MessageList(
viewModel = messageListViewModel,
// Messages are automatically limited based on your MessageLimitConfig
)For custom implementations, observe the message state from ChannelState:
val channelState: StateFlow<ChannelState?> = chatClient.watchChannelAsState(cid, messageLimit = 30)
channelState
.filterNotNull()
.flatMapLatest { it.messages }
.collectLatest { messages ->
// Messages are already limited based on your configuration
updateUI(messages)
}Buffering New Message Events
By default, every incoming WebSocket event flows through a single, unbounded sequential pipeline. Under livestream-scale load, message.new events can arrive faster than that pipeline can drain them, which grows memory and increases the latency between the server delivering an event and the SDK applying it to local state.
MessageBufferConfig lets you route NewMessageEvents for specific channel types through a bounded buffer with a configurable overflow strategy. All other event types — and all events for other channel types — continue to use the standard unbuffered path, so signal-critical events like reads, bans, and member updates are never dropped.
By default this configuration is a no-op: with no channel types opted in, the buffered code path is not active.
Configuration
MessageBufferConfig is nested under MessageLimitConfig and applied via ChatClientConfig:
val chatClientConfig = ChatClientConfig(
messageLimitConfig = MessageLimitConfig(
messageBufferConfig = MessageBufferConfig(
channelTypes = setOf("livestream"),
capacity = 100,
overflow = MessageBufferOverflow.DROP_OLDEST,
),
),
)
ChatClient.Builder(apiKey, context)
.config(chatClientConfig)
.build()Options
channelTypes— the channel types whoseNewMessageEvents should be routed through the bounded buffer. Channel types outside this set continue to use the unbuffered path. An empty set (the default) disables buffering entirely.capacity— the maximum number ofNewMessageEvents that can be queued while the consumer is busy. Once exceeded,overflowdecides which event to drop. Defaults toInt.MAX_VALUE, which effectively disables overflow.overflow— the strategy applied when the buffer is full:MessageBufferOverflow.DROP_OLDEST(default): evict the oldest queued event to make room for the new one. Useful for live channels where freshness matters more than completeness.MessageBufferOverflow.DROP_LATEST: discard the newest event and keep the queued events.
Pitfalls
MessageBufferConfig is a deliberate trade-off — performance and freshness in exchange for two relaxed guarantees. Make sure these are acceptable for your use case before opting in:
- Messages can be lost. When the buffer is full, the SDK drops a
NewMessageEventto keep the pipeline moving. A dropped message will not appear locally until something else (pagination, a freshwatch, a sync) brings it back from the server. Do not enable this for channels where every message must be delivered to the client. - Message ordering is no longer guaranteed. Buffered
NewMessageEvents flow through a separate queue from every other event. As a consequence, aReactionNewEvent,MessageUpdatedEvent, orMessageDeletedEventreferencing message X may be processed before theNewMessageEventthat creates X locally. If your UI relies on strict event ordering (for example, assuming a message exists before any update for it arrives), it must tolerate this relaxation. - Only
NewMessageEvents are buffered. Reads, bans, member updates and other event types are never dropped — but they are also no longer ordered with respect to buffered new messages on the same channel. - Default is no-op. An empty
channelTypes(the default) disables buffering entirely, so existing apps see no behavior change unless they explicitly opt in.
Fast Event Parsing
ChatClientConfig.fastEventParsing enables a direct WebSocket event parser. Supported event types are parsed straight into the domain model, bypassing the intermediate DTO layer. This reduces per-event allocations and CPU time — a meaningful win at livestream scale.
Currently supported event types:
message.new
Unsupported event types transparently fall back to the standard parser, so enabling the flag is safe even as the set of supported types grows in future releases.
Configuration
val chatClientConfig = ChatClientConfig(
fastEventParsing = true,
)
ChatClient.Builder(apiKey, context)
.config(chatClientConfig)
.build()Disabled by default.
Skipping Database Storage
For channels where message persistence is not needed (e.g., ephemeral livestream messages), you can configure offline support to skip database storage for specific channel types. This reduces disk I/O and improves performance.
Configuration
Configure ignored channel types when setting up the client:
ChatClient.Builder(apiKey, context)
.config(
ChatClientConfig(
// Messages in these channel types won't be stored in the local database
ignoredOfflineChannelTypes = setOf("livestream"),
)
)
.build()Behavior
When a channel type is in ignoredOfflineChannelTypes:
- Messages are not persisted to the local database
- Reactions in these channels are not stored locally
- The channel still receives real-time updates via WebSocket
This is useful for high-volume channels where:
- Messages are ephemeral and don't need offline access
- Reducing database writes improves performance
- Storage space is a concern
Complete Setup Example
Here's a complete example combining all of the optimizations above for a livestream use case:
val chatClientConfig = ChatClientConfig(
messageLimitConfig = MessageLimitConfig(
channelMessageLimits = setOf(
ChannelMessageLimit(channelType = "livestream", baseLimit = 500),
),
messageBufferConfig = MessageBufferConfig(
channelTypes = setOf("livestream"),
capacity = 100,
overflow = MessageBufferOverflow.DROP_OLDEST,
),
),
ignoredOfflineChannelTypes = setOf("livestream"),
fastEventParsing = true,
)
ChatClient.Builder(apiKey, context)
.config(chatClientConfig)
.build()With this configuration:
- Livestream channels keep only the 500 most recent messages in memory
- New-message events on livestream channels are bounded by the configured buffer, dropping the oldest pending event on overflow
message.newevents are decoded via the fast parser- Messages are not written to the local database
- Real-time updates continue to work normally
- Memory and disk usage remain controlled even with high message volume