Channel lists are a core part of most messaging applications, and our SDKs make them easy to build using the Channel List components. These lists are powered by the Query Channels API, which retrieves channels based on filter criteria, sorting options, and pagination settings.
Here's an example of how you can query the list of channels:
JavaScript Kotlin Java (Android) Swift Dart Unity Unreal Python Ruby PHP Go C# Java
const filter = { type: "messaging" , members: { $in: [ "thierry" ] } };
const sort = [{ last_message_at: - 1 }];
const options = { limit: 15 };
const channels = await chatClient. queryChannels (filter, sort, options); val request = QueryChannelsRequest (
filter = Filters. and (
Filters. eq ( "type" , "messaging" ),
Filters. `in` ( "members" , listOf ( "thierry" )),
),
offset = 0 ,
limit = 10 ,
querySort = QuerySortByField. descByName ( "lastMessageAt" )
). apply {
watch = true
state = true
}
client. queryChannels (request). enqueue { result ->
if (result is Result.Success) {
val channels: List < Channel > = result. value
} else {
// Handle Result.Failure
}
} FilterObject filter = Filters. and (
Filters. eq ( "type" , "messaging" ),
Filters. in ( "members" , Arrays. asList ( "thierry" ))
);
int offset = 0 ;
int limit = 10 ;
QuerySortByField< Channel > sort = QuerySortByField. descByName ( "lastMessageAt" );
int messageLimit = 0 ;
int memberLimit = 0 ;
QueryChannelsRequest request = new QueryChannelsRequest (filter, offset, limit, sort, messageLimit, memberLimit)
. withWatch ()
. withState ();
client. queryChannels (request). enqueue (result -> {
if (result. isSuccess ()) {
List< Channel > channels = result. getOrNull ();
} else {
// Handle error
}
}); let controller = chatClient. channelListController (
query : . init (
filter : . and ([. equal (.type, to : .messaging), . containMembers ( userIds : [ "thierry" ])]),
sort : [. init ( key : .lastMessageAt, isAscending : false )],
pageSize : 10
)
)
controller. synchronize { error in
if let error = error {
// handle error
print (error)
} else {
// access channels
print (controller.channels)
// load more if needed
controller. loadNextChannels ( limit : 10 ) { error in
// handle error / access channels
}
}
} final filter = Filter . in_ ( 'members' ,[ 'thierry' ]);
final sort = [ SortOption < ChannelState >. desc ( 'last_message_at' )];
final channels = await client. queryChannels (
filter : filter,
channelStateSort : sort,
watch : true ,
state : true ,
).last;
channels. forEach (( Channel c) {
print ( " ${ c . extraData [ 'name' ]} ${ c . cid } " );
}); var filters = new List < IFieldFilterRule >
{
// Return only channels where local user is a member
ChannelFilter.Members. In (Client.LocalUserData.UserId),
};
var channels = await Client. QueryChannelsAsync (filters); const FFilter Filter = FFilter :: And ({
FFilter :: In ( TEXT ( "members" ), { TEXT ( "thierry" )}),
FFilter :: Equal ( TEXT ( "type" ), TEXT ( "messaging" )),
});
const TArray < FChannelSortOption > SortOptions{{ EChannelSortField ::LastMessageAt, ESortDirection ::Descending}};
Client-> QueryChannels (
Filter,
SortOptions,
EChannelFlags ::State,
{}, // Pagination options
[]( const TArray < UChatChannel * > ReceivedChannels )
{
// Do something with ReceivedChannels
}); from getstream.models import SortParamRequest
client.chat.query_channels(
filter_conditions = { "members" : { "$in" : [ "elon" , "jack" , "jessie" ]}},
sort = [SortParamRequest( field = "last_message_at" , direction = 1 )],
limit = 10 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
client. chat . query_channels (
Models :: QueryChannelsRequest . new (
filter_conditions: { 'members' => { '$in' => [ 'elon' , 'jack' , 'jessie' ] } },
sort: [ Models :: SortParamRequest . new ( field: 'last_message_at' , direction: 1 )],
limit: 10
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
filterConditions : ( object )[ "members" => ( object )[ '$in' => [ "elon" , "jack" , "jessie" ]]],
sort : [ new Models\SortParamRequest ( field : "last_message_at" , direction : 1 )],
limit : 10 ,
)); resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
FilterConditions: map [ string ] any {
"members" : map [ string ] any {
"$in" : [] string { "elon" , "jack" , "jessie" },
},
},
Sort: [] getstream . SortParamRequest {{Field: getstream. PtrTo ( "last_message_at" ), Direction: getstream. PtrTo ( 1 )}},
Limit: getstream. PtrTo ( 10 ),
}) using GetStream ;
using GetStream . Models ;
var client = new StreamClient ( "{{ api_key }}" , "{{ api_secret }}" );
var chat = new ChatClient (client);
var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
FilterConditions = new Dictionary < string , object >
{
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { "elon" , "jack" , "jessie" } }
},
Sort = new List < SortParamRequest >
{
new SortParamRequest { Field = "last_message_at" , Direction = 1 }
},
Limit = 10
}); chat. queryChannels (QueryChannelsRequest. builder ()
. filterConditions (Map. of (
"type" , "messaging" ,
"members" , Map. of ( "$in" , List. of ( "thierry" ))))
. sort (List. of (SortParamRequest. builder (). field ( "last_message_at" ). direction ( - 1 ). build ()))
. state ( true )
. build ()). execute ();
Name Type Description Default Optional filters object Filter criteria for channel fields. See Queryable Fields for available options. {}sort object or array of objects Sorting criteria based on field and direction. You can sort by last_updated , last_message_at , updated_at , created_at , member_count , unread_count , or has_unread . Direction can be ascending (1) or descending (-1). Multiple sort options can be provided. [{last_updated: -1}]options object Additional query options. See Query Options . {}
An empty filter matches all channels in your application. In production, always include at least members: { $in: [userID] } to return only channels the user belongs to.
The API only returns channels that the user has permission to read. For messaging channels, this typically means the user must be a member. Include appropriate filters to match your channel type's permission model.
Understanding which filters perform well at scale helps you build efficient channel queries. This section covers common filter patterns with their performance characteristics.
Performance Summary : Filters using indexed fields (cid, type, members, last_message_at) perform best. See Performance Considerations for detailed guidance.
For most messaging applications, filter by channel type and membership. This pattern uses indexed fields and performs well at scale.
High membership counts : For users with a large number of channel memberships (more than a few thousand), filtering by members: { $in: [userID] } becomes less selective and may cause performance issues. In these cases, consider adding additional filters (like last_message_at) to narrow the result set.
JavaScript Kotlin Java (Android) Swift Dart Unity Unreal Python Ruby PHP Go C# Java
const filter = { members: { $in: [ "thierry" ] }, type: "messaging" }; val filter = Filters. and (
Filters. eq ( "type" , "messaging" ),
Filters. `in` ( "members" , listOf ( "thierry" )),
) FilterObject filter = Filters. and (
Filters. eq ( "type" , "messaging" ),
Filters. in ( "members" , Arrays. asList ( "thierry" ))
); let currentUserChannels = chatClient. channelListController (
query : . init (
filter : . and ([
. equal (.type, to : .messaging),
. containMembers ( userIds : [chatClient.currentUserId ! ])
])
)
)
currentUserChannels. synchronize () final filter = Filter . and ([
Filter . equal ( 'type' , 'messaging' ),
Filter . in_ ( 'members' , [ 'thierry' ]),
]); var filters = new List < IFieldFilterRule >
{
ChannelFilter.Type. EqualsTo (ChannelType.Messaging),
ChannelFilter.Members. In (Client.LocalUserData.UserId),
};
var channels = await Client. QueryChannelsAsync (filters); const FFilter Filter = FFilter :: And ({
FFilter :: Equal ( TEXT ( "type" ), TEXT ( "messaging" )),
FFilter :: In ( TEXT ( "members" ), { TEXT ( "thierry" )}),
}); filter = { "type" : "messaging" , "members" : { "$in" : [ "thierry" ]}} require 'getstream_ruby'
filter = { 'type' => 'messaging' , 'members' => { '$in' => [ 'thierry' ] } } $filter = ( object )[ "members" => ( object )[ '$in' => [ "thierry" ]], "type" => "messaging" ]; filter := map [ string ] any {
"type" : "messaging" ,
"members" : map [ string ] any {
"$in" : [] string { "thierry" },
},
} var filter = new Dictionary < string , object >
{
[ "type" ] = "messaging" ,
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { "thierry" } }
}; Map< String , Object > filter = Map. of (
"type" , "messaging" ,
"members" , Map. of ( "$in" , List. of ( "thierry" )));
The following fields can be used in your filter criteria:
Name Type Description Supported Operators Example frozen boolean Channel frozen status $eq false type string or list of string Channel type $in, $eq messaging cid string or list of string Full channel ID (type:id) $in, $eq messaging:general members list of string User IDs of channel members $in [thierry, marcelo] invite string (pending, accepted, rejected) Invite status $eq pending joined boolean Whether the current user has joined the channel $eq true muted boolean Whether the current user has muted the channel $eq true member.user.name string Name property of a channel member $autocomplete, $eq marc created_by_id string ID of the user who created the channel $eq marcelo hidden boolean Whether the current user has hidden the channel $eq false last_message_at string (RFC3339 timestamp) Time of the last message $eq, $gt, $lt, $gte, $lte, $exists 2021-01-15T09:30:20.45Z member_count integer Number of members $eq, $gt, $lt, $gte, $lte 5 created_at string (RFC3339 timestamp) Channel creation time $eq, $gt, $lt, $gte, $lte, $exists 2021-01-15T09:30:20.45Z updated_at string (RFC3339 timestamp) Channel update time $eq, $gt, $lt, $gte, $lte 2021-01-15T09:30:20.45Z team string Team associated with the channel $eq stream last_updated string (RFC3339 timestamp) Time of last message, or channel creation time if no messages exist $eq, $gt, $lt, $gte, $lte 2021-01-15T09:30:20.45Z disabled boolean Channel disabled status $eq false has_unread boolean Whether the user has unread messages (only true supported, max 100 channels) true true app_banned string Filter by application-banned users (only for 2-member channels) excluded, only excluded
The app_banned filter only works on direct message channels with exactly 2 members.
Name Type Description Default Optional state boolean Return channel state true ✓ watch boolean Subscribe to real-time updates for returned channels true ✓ limit integer Number of channels to return (max 30) 10 ✓ offset integer Number of channels to skip (max 1000) 0 ✓ message_limit integer Messages to include per channel (max 300) 25 ✓ member_limit integer Members to include per channel (max 100) 100 ✓
Performance Tip : Setting state: false and watch: false reduces response size and processing time. Use these options when you only need channel IDs or basic metadata—for example, during background syncs, administrative operations, or when building lightweight channel lists that don't require full state.
The API returns a list of ChannelState objects containing all information needed to render channels without additional API calls.
Field Name Description channel Channel data messages Recent messages (based on message_limit) watcher_count Number of users currently watching read Read state for up to 100 members, ordered by most recently added (current user's read state is always included) members Up to 100 members, ordered by most recently added pinned_messages Up to 10 most recent pinned messages
Example Response [
{
"id" : "f8IOxxbt" ,
"type" : "messaging" ,
"cid" : "messaging:f8IOxxbt" ,
"last_message_at" : "2020-01-10T07:26:46.791232Z" ,
"created_at" : "2020-01-10T07:25:37.63256Z" ,
"updated_at" : "2020-01-10T07:25:37.632561Z" ,
"created_by" : {
"id" : "8ce4c6e11118ca103a0a7c633dcf60dd" ,
"role" : "admin" ,
"created_at" : "2019-08-27T17:33:14.442265Z" ,
"updated_at" : "2020-01-10T07:25:36.402819Z" ,
"last_active" : "2020-01-10T07:25:36.395796Z" ,
"banned" : false ,
"online" : false ,
"image" : "https://ui-avatars.com/api/?name=mezie&size=192&background=000000&color=6E7FFE&length=1" ,
"name" : "mezie" ,
"username" : "mezie"
},
"frozen" : false ,
"config" : {
"created_at" : "2020-01-20T10:23:44.878185331Z" ,
"updated_at" : "2020-01-20T10:23:44.878185458Z" ,
"name" : "messaging" ,
"typing_events" : true ,
"read_events" : true ,
"connect_events" : true ,
"search" : true ,
"reactions" : true ,
"replies" : true ,
"mutes" : true ,
"uploads" : true ,
"url_enrichment" : true ,
"max_message_length" : 5000 ,
"automod" : "disabled" ,
"automod_behavior" : "flag" ,
"commands" : [
{
"name" : "giphy" ,
"description" : "Post a random gif to the channel" ,
"args" : "[text]" ,
"set" : "fun_set"
}
]
},
"name" : "Video Call"
}
]
Use limit and offset to paginate through results:
JavaScript Kotlin Java (Android) Swift Dart Unity Unreal Python Ruby PHP Go C# Java
const filter = { members: { $in: [ "thierry" ] } };
const sort = { last_message_at: - 1 };
// Get channels 11-30
const channels = await authClient. queryChannels (filter, sort, {
limit: 20 ,
offset: 10 ,
}); // Get the first 10 channels
val filter = Filters. `in` ( "members" , "thierry" )
val offset = 0
val limit = 10
val request = QueryChannelsRequest (filter, offset, limit)
client. queryChannels (request). enqueue { result ->
if (result is Result.Success) {
val channels = result. value
} else {
// Handle Result.Failure
}
}
// Get the second 10 channels
val nextRequest = QueryChannelsRequest (
filter = filter,
offset = 10 , // Skips first 10
limit = limit
)
client. queryChannels (nextRequest). enqueue { result ->
if (result is Result.Success) {
val channels = result. value
} else {
// Handle Result.Failure
}
} // Get the first 10 channels
FilterObject filter = Filters. in ( "members" , "thierry" );
int offset = 0 ;
int limit = 10 ;
QuerySorter< Channel > sort = new QuerySortByField<>();
int messageLimit = 0 ;
int memberLimit = 0 ;
QueryChannelsRequest request = new QueryChannelsRequest (filter, offset, limit, sort, messageLimit, memberLimit);
client. queryChannels (request). enqueue (result -> {
if (result. isSuccess ()) {
List< Channel > channels = result. getOrNull ();
} else {
// Handle error
}
});
// Get the second 10 channels
int nextOffset = 10 ; // Skips first 10
QueryChannelsRequest nextRequest = new QueryChannelsRequest (filter, nextOffset, limit, sort, messageLimit, memberLimit);
client. queryChannels (nextRequest). enqueue (result -> {
if (result. isSuccess ()) {
List< Channel > channels = result. getOrNull ();
} else {
// Handle error
}
}); let controller = chatClient. channelListController (
query : . init (
filter : . containMembers ( userIds : [ "thierry" ]),
pageSize : 10
)
)
// Get the first 10 channels
controller. synchronize { error in
if let error = error {
// handle error
print (error)
} else {
// Access channels
print (controller.channels)
// Get the next 10 channels
controller. loadNextChannels { error in
// handle error / access channels
print (error ?? controller.channels)
}
}
} final filter = Filter . in_ ( 'members' , [ 'thierry' ]);
final sort = [ SortOption < ChannelState >. desc ( 'last_message_at' )];
final channels = await client. queryChannels (
filter : filter,
channelStateSort : sort,
paginationParams : PaginationParams (
limit : 20 ,
offset : 10 ,
),
).last;
for ( final channel in channels) {
print ( ' ${ channel . extraData [ 'name' ]} ${ channel . cid } ' );
} var filters = new List < IFieldFilterRule >
{
// Return only channels where local user is a member
ChannelFilter.Members. In (Client.LocalUserData.UserId),
};
// Pass limit and offset to control pagination
// Limit - records per page
// Offset - records to skip
var channels = await Client. QueryChannelsAsync (filters, limit : 30 , offset : 60 ); const FFilter Filter = FFilter :: In ( TEXT ( "members" ), { TEXT ( "thierry" )});
const TArray < FChannelSortOption > SortOptions{
{ EChannelSortField ::LastMessageAt, ESortDirection ::Descending},
};
const FPaginationOptions PaginationOptions{
20 , // Limit
10 , // Offset
};
Client-> QueryChannels (
Filter,
SortOptions,
EChannelFlags ::State,
PaginationOptions,
[]( const TArray < UChatChannel * > ReceivedChannels )
{
// Started watching channels
}); from getstream.models import SortParamRequest
client.chat.query_channels(
filter_conditions = { "members" : { "$in" : [ "thierry" ]}},
sort = [SortParamRequest( field = "last_message_at" , direction =- 1 )],
limit = 20 ,
offset = 10 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
client. chat . query_channels (
Models :: QueryChannelsRequest . new (
filter_conditions: { 'members' => { '$in' => [ 'thierry' ] } },
sort: [ Models :: SortParamRequest . new ( field: 'last_message_at' , direction: - 1 )],
limit: 20 ,
offset: 10
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
filterConditions : ( object )[ "members" => ( object )[ '$in' => [ "thierry" ]]],
sort : [ new Models\SortParamRequest ( field : "last_message_at" , direction : - 1 )],
limit : 20 ,
offset : 10 ,
)); resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
FilterConditions: map [ string ] any {
"members" : map [ string ] any {
"$in" : [] string { "thierry" },
},
},
Sort: [] getstream . SortParamRequest {{Field: getstream. PtrTo ( "last_message_at" ), Direction: getstream. PtrTo ( - 1 )}},
Limit: getstream. PtrTo ( 20 ),
Offset: getstream. PtrTo ( 10 ),
}) var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
FilterConditions = new Dictionary < string , object >
{
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { "thierry" } }
},
Sort = new List < SortParamRequest >
{
new SortParamRequest { Field = "last_message_at" , Direction = - 1 }
},
Limit = 20 ,
Offset = 10
}); // Get first 10 channels
Map< String , Object > filter = Map. of ( "members" , Map. of ( "$in" , List. of ( "thierry" )));
var channels = chat. queryChannels (QueryChannelsRequest. builder ()
. filterConditions (filter)
. state ( true )
. limit ( 10 )
. offset ( 0 )
. build ()). execute ();
// Get the second 10 channels
var nextChannels = chat. queryChannels (QueryChannelsRequest. builder ()
. filterConditions (filter)
. state ( true )
. limit ( 10 )
. offset ( 10 )
. build ()). execute ();
Always include members: { $in: [userID] } in your filter to ensure consistent pagination results. Without this filter, channel list changes may cause pagination issues.
A channel is not created in the API until one of the following methods is called. Each method has subtle differences:
JavaScript Kotlin Dart Unity
channel. create ();
channel. query ();
channel. watch (); channelClient. create (memberIds = listOf ( "user-1" , "user-2" ), extraData = emptyMap ()). enqueue ()
channelClient. query ( QueryChannelRequest ()). enqueue ()
channelClient. watch (). enqueue () await channel. create ();
await channel. query ();
await channel. watch (); // Get channel state or create one if it doesn't exist
var channel = await Client. GetOrCreateChannelWithIdAsync (ChannelType.Messaging, "general" );
// Or get multiple channels matching a filter
var channels = await Client. QueryChannelsAsync (filters);
Only one of these is necessary. For example, calling watch automatically creates the channel in addition to subscribing to real-time updates—there's no need to call create separately.
With queryChannels, a user can watch up to 30 channels in a single API call. This eliminates the need to watch channels individually using channel.watch() after querying. Using queryChannels can substantially decrease API calls, reducing network traffic and improving performance when working with many channels.
Channel lists often form the backbone of the chat experience and are typically one of the first views users see. Use the most selective filter possible:
Filter by CID is the most performant query you can use
For social messaging (DMs, group chats), use at minimum type and members: { $in: [userID] }
Avoid overly complex queries with more than one AND or OR statement
Filtering by type alone is not recommended—always include additional criteria
Use Predefined Filters in production for frequently used query patterns
JavaScript Kotlin Dart
// Most performant: Filter by CID
const filter = { cid: channelCID };
// Recommended for social messaging
const filter = { type: "messaging" , members: { $in: [userID] } };
// Not recommended: type alone
const filter = { type: "messaging" }; // Most performant: Filter by CID
val filter = Filters. eq ( "cid" , channelCid)
// Recommended for social messaging
val filter = Filters. and (
Filters. eq ( "type" , "messaging" ),
Filters. `in` ( "members" , listOf (userId)),
)
// Not recommended: type alone
val filter = Filters. eq ( "type" , "messaging" ) // Most performant: Filter by CID
final filter = Filter . equal ( 'cid' , channelCid);
// Recommended for social messaging
final filter = Filter . and ([
Filter . equal ( 'type' , 'messaging' ),
Filter . in_ ( 'members' , [userId]),
]);
// Not recommended: type alone
final filter = Filter . equal ( 'type' , 'messaging' );
If your filter returns more than a few thousand channels, consider adding more selective criteria. For frequently used query patterns, use Predefined Filters to enable performance monitoring through the Dashboard. Contact support if you plan on having millions of channels and need guidance on optimal filters.
Always specify a sort parameter in your query. The default is last_updated (the more recent of created_at and last_message_at).
The most optimized sort options are:
last_updated (default)
last_message_at
JavaScript Kotlin Dart Unity
const sort = { last_message_at: - 1 }; val sort = QuerySortByField. descByName < Channel >( "last_message_at" ) final sort = [ SortOption < ChannelState >. desc ( 'last_message_at' )]; var sort = ChannelSort. OrderByDescending (ChannelSortFieldName.LastMessageAt);
var channels = await Client. QueryChannelsAsync (filters, sort);
For the full list of supported query operators, see Query Syntax Operators .
Following recommended patterns helps ensure your queries perform well as your application scales. Here are examples of good and bad query patterns for all server-side SDKs.
Use indexed fields like type, members, and last_message_at for efficient queries:
Node.js Python Ruby PHP Go C# Java
// ✅ GOOD: Selective filter using indexed fields
const filter = {
type: "messaging" ,
members: { $in: [userId] },
last_message_at: { $gte: thirtyDaysAgo },
};
const sort = { last_message_at: - 1 };
const channels = await serverClient. queryChannels (filter, sort, { limit: 20 }); from getstream.models import SortParamRequest
# ✅ GOOD: Selective filter using indexed fields
channels = client.chat.query_channels(
filter_conditions = {
"type" : "messaging" ,
"members" : { "$in" : [user_id]},
"last_message_at" : { "$gte" : thirty_days_ago},
},
sort = [SortParamRequest( field = "last_message_at" , direction =- 1 )],
limit = 20 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
# ✅ GOOD: Selective filter using indexed fields
filter = {
'type' => 'messaging' ,
'members' => { '$in' => [user_id] },
'last_message_at' => { '$gte' => thirty_days_ago }
}
channels = client. chat . query_channels (
Models :: QueryChannelsRequest . new (
filter_conditions: filter,
sort: [ Models :: SortParamRequest . new ( field: 'last_message_at' , direction: - 1 )],
limit: 20
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
// ✅ GOOD: Selective filter using indexed fields
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
filterConditions : ( object )[
"type" => "messaging" ,
"members" => ( object )[ '$in' => [$userId]],
"last_message_at" => ( object )[ '$gte' => $thirtyDaysAgo],
],
sort : [ new Models\SortParamRequest ( field : "last_message_at" , direction : - 1 )],
limit : 20 ,
)); // ✅ GOOD: Selective filter using indexed fields
filter := map [ string ] any {
"type" : "messaging" ,
"members" : map [ string ] any {
"$in" : [] string {userID},
},
"last_message_at" : map [ string ] any {
"$gte" : thirtyDaysAgo,
},
}
resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
FilterConditions: filter,
Sort: [] getstream . SortParamRequest {{Field: getstream. PtrTo ( "last_message_at" ), Direction: getstream. PtrTo ( - 1 )}},
Limit: getstream. PtrTo ( 20 ),
}) // ✅ GOOD: Selective filter using indexed fields
var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
FilterConditions = new Dictionary < string , object >
{
[ "type" ] = "messaging" ,
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { userId } },
[ "last_message_at" ] = new Dictionary < string , object > { [ "$gte" ] = thirtyDaysAgo }
},
Sort = new List < SortParamRequest >
{
new SortParamRequest { Field = "last_message_at" , Direction = - 1 }
},
Limit = 20
}); // Backend SDK
// ✅ GOOD: Selective filter using indexed fields
Map< String , Object > filter = Map. of (
"type" , "messaging" ,
"members" , Map. of ( "$in" , List. of (userId)),
"last_message_at" , Map. of ( "$gte" , thirtyDaysAgo));
chat. queryChannels (QueryChannelsRequest. builder ()
. filterConditions (filter)
. sort (List. of (SortParamRequest. builder (). field ( "last_message_at" ). direction ( - 1 ). build ()))
. limit ( 20 )
. build ()). execute ();
Avoid overly broad filters or deep nesting of logical operators, which can cause performance issues at scale and may result in dynamic rate limiting:
JavaScript Python Ruby PHP Go C# Java
// ❌ BAD: Type-only filter (too broad)
const broadFilter = { type: "messaging" };
// ❌ BAD: Deep nesting of logical operators
const nestedFilter = {
$and: [
{
$or: [{ frozen: true }, { disabled: true }],
},
{
$or: [{ hidden: true }, { muted: true }],
},
],
}; # ❌ BAD: Type-only filter (too broad)
broad_filter = { "type" : "messaging" }
# ❌ BAD: Deep nesting of logical operators
nested_filter = {
"$and" : [
{
"$or" : [
{ "frozen" : True },
{ "disabled" : True },
]
},
{
"$or" : [
{ "hidden" : True },
{ "muted" : True },
]
},
]
} require 'getstream_ruby'
# ❌ BAD: Type-only filter (too broad)
broad_filter = { 'type' => 'messaging' }
# ❌ BAD: Deep nesting of logical operators
nested_filter = {
'$and' => [
{
'$or' => [
{ 'frozen' => true },
{ 'disabled' => true }
]
},
{
'$or' => [
{ 'hidden' => true },
{ 'muted' => true }
]
}
]
} // ❌ BAD: Type-only filter (too broad)
$broadFilter = ( object )[ "type" => "messaging" ];
// ❌ BAD: Deep nesting of logical operators
$nestedFilter = ( object )[
'$and' => [
( object )[
'$or' => [
( object )[ "frozen" => true ],
( object )[ "disabled" => true ],
],
],
( object )[
'$or' => [
( object )[ "hidden" => true ],
( object )[ "muted" => true ],
],
],
],
]; // ❌ BAD: Type-only filter (too broad)
broadFilter := map [ string ] any {
"type" : "messaging" ,
}
// ❌ BAD: Deep nesting of logical operators
nestedFilter := map [ string ] any {
"$and" : [] map [ string ] any {
{
"$or" : [] map [ string ] any {
{ "frozen" : true },
{ "disabled" : true },
},
},
{
"$or" : [] map [ string ] any {
{ "hidden" : true },
{ "muted" : true },
},
},
},
} // ❌ BAD: Type-only filter (too broad)
var broadFilter = new Dictionary < string , object >
{
[ "type" ] = "messaging"
};
// ❌ BAD: Deep nesting of logical operators
var nestedFilter = new Dictionary < string , object >
{
[ "$and" ] = new []
{
new Dictionary < string , object >
{
[ "$or" ] = new []
{
new Dictionary < string , object > { [ "frozen" ] = true },
new Dictionary < string , object > { [ "disabled" ] = true }
}
},
new Dictionary < string , object >
{
[ "$or" ] = new []
{
new Dictionary < string , object > { [ "hidden" ] = true },
new Dictionary < string , object > { [ "muted" ] = true }
}
}
}
}; // Backend SDK
// ❌ BAD: Type-only filter (too broad)
Map< String , Object > broadFilter = Map. of ( "type" , "messaging" );
// ❌ BAD: Deep nesting of logical operators
Map< String , Object > nestedFilter = Map. of (
"$and" , List. of (
Map. of ( "$or" , List. of (
Map. of ( "frozen" , true ),
Map. of ( "disabled" , true ))),
Map. of ( "$or" , List. of (
Map. of ( "hidden" , true ),
Map. of ( "muted" , true )))));
For frequently used query patterns, use Predefined Filters in production. They provide several benefits:
Consistency : Define filter logic once and reuse across your application
Performance Monitoring : View performance analysis through the Dashboard
Optimization Insights : Receive recommendations for improving slow queries
Kotlin Swift Dart Node.js Python Ruby PHP Go C# Java
// The filter and sort templates are configured server-side in the Dashboard.
val request = QueryChannelsRequest (
predefinedFilter = "user_messaging_channels" ,
filterValues = mapOf ( "user_id" to "user123" ),
limit = 20 ,
)
client. queryChannels (request). enqueue () // The filter and sort templates are configured server-side in the Dashboard.
let controller = chatClient. channelListController (
query : . init (
predefinedFilter : "user_messaging_channels" ,
filterValues : [ "user_id" : . string (userId)]
)
)
controller. synchronize () // The filter and sort templates are configured server-side in the Dashboard.
final channels = await client. queryChannelsOnline (
predefinedFilter : 'user_messaging_channels' ,
filterValues : { 'user_id' : userId},
paginationParams : const PaginationParams (limit : 20 ),
); // Production-ready: Use Predefined Filter
const channels = await serverClient. queryChannels (
{}, // filter_conditions ignored with predefined_filter
{ last_message_at: - 1 },
{
predefined_filter: "user_messaging_channels" ,
filter_values: { user_id: userId },
limit: 20 ,
},
); from getstream.models import SortParamRequest
# Production-ready: Use Predefined Filter
channels = client.chat.query_channels(
predefined_filter = "user_messaging_channels" ,
filter_values = { "user_id" : user_id},
sort = [SortParamRequest( field = "last_message_at" , direction =- 1 )],
limit = 20 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
# Production-ready: Use Predefined Filter
channels = client. chat . query_channels (
Models :: QueryChannelsRequest . new (
predefined_filter: 'user_messaging_channels' ,
filter_values: { 'user_id' => user_id },
sort: [ Models :: SortParamRequest . new ( field: 'last_message_at' , direction: - 1 )],
limit: 20
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
// Production-ready: Use Predefined Filter
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
predefinedFilter : "user_messaging_channels" ,
filterValues : ( object )[ "user_id" => $userId],
sort : [ new Models\SortParamRequest ( field : "last_message_at" , direction : - 1 )],
limit : 20 ,
)); // Production-ready: Use Predefined Filter
resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
PredefinedFilter: getstream. PtrTo ( "user_messaging_channels" ),
FilterValues: map [ string ] any {
"user_id" : userID,
},
Sort: [] getstream . SortParamRequest {{Field: getstream. PtrTo ( "last_message_at" ), Direction: getstream. PtrTo ( - 1 )}},
Limit: getstream. PtrTo ( 20 ),
}) // Production-ready: Use Predefined Filter
var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
PredefinedFilter = "user_messaging_channels" ,
FilterValues = new Dictionary < string , object > { [ "user_id" ] = userId },
Sort = new List < SortParamRequest >
{
new SortParamRequest { Field = "last_message_at" , Direction = - 1 }
},
Limit = 20
}); // Backend SDK
// Production-ready: Use Predefined Filter
chat. queryChannels (QueryChannelsRequest. builder ()
. predefinedFilter ( "user_messaging_channels" )
. filterValues (Map. of ( "user_id" , userId))
. sort (List. of (SortParamRequest. builder (). field ( "last_message_at" ). direction ( - 1 ). build ()))
. limit ( 20 )
. build ()). execute ();
Use the Stream Dashboard to monitor and optimize your QueryChannels performance:
Create Predefined Filters for your frequently used query patterns
View Performance Analysis in the Dashboard once filters receive traffic
Review Recommendations for optimization opportunities
Track Improvements over time as you optimize your queries
Performance insights availability : Performance scores and recommendations become available once a filter/sort combination receives significant traffic. Not all filters will show analysis immediately—the system needs sufficient usage data to provide meaningful insights.
Predefined Filters are reusable, templated filter configurations that you create and manage in the Stream Dashboard . They provide a recommended approach for production QueryChannels usage.
Consistency : Define filter logic once and reuse it across your application
Dashboard Management : Create, update, and monitor filters through the Dashboard
Performance Insights : View performance analysis for your filters directly in the Dashboard once they receive significant traffic
Dynamic Values : Use placeholders for values that change at query time (like user IDs)
Create and manage Predefined Filters in the Stream Dashboard . Navigate to your app's settings to define filter templates with placeholders for dynamic values.
Reference a predefined filter by name and provide values for any placeholders:
Kotlin Swift Dart Python Ruby PHP Go C# Java
val request = QueryChannelsRequest (
predefinedFilter = "user_messaging_channels" ,
filterValues = mapOf ( "user_id" to "user123" ),
limit = 20 ,
)
client. queryChannels (request). enqueue () let controller = chatClient. channelListController (
query : . init (
predefinedFilter : "user_messaging_channels" ,
filterValues : [ "user_id" : . string ( "user123" )]
)
)
controller. synchronize () final channels = await client. queryChannelsOnline (
predefinedFilter : 'user_messaging_channels' ,
filterValues : { 'user_id' : 'user123' },
paginationParams : const PaginationParams (limit : 20 ),
); channels = client.chat.query_channels(
predefined_filter = "user_messaging_channels" ,
filter_values = { "user_id" : "user123" },
limit = 20 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
channels = client. chat . query_channels (
Models :: QueryChannelsRequest . new (
predefined_filter: 'user_messaging_channels' ,
filter_values: { 'user_id' => 'user123' },
limit: 20
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
predefinedFilter : "user_messaging_channels" ,
filterValues : ( object )[ "user_id" => "user123" ],
limit : 20 ,
)); resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
PredefinedFilter: getstream. PtrTo ( "user_messaging_channels" ),
FilterValues: map [ string ] any {
"user_id" : "user123" ,
},
Limit: getstream. PtrTo ( 20 ),
}) var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
PredefinedFilter = "user_messaging_channels" ,
FilterValues = new Dictionary < string , object > { [ "user_id" ] = "user123" },
Limit = 20
}); // Backend SDK
chat. queryChannels (QueryChannelsRequest. builder ()
. predefinedFilter ( "user_messaging_channels" )
. filterValues (Map. of ( "user_id" , "user123" ))
. limit ( 20 )
. build ()). execute ();
Placeholders use double curly braces: {{placeholder_name}}
When creating a predefined filter in the Dashboard, you can define templates like:
{
"type" : "{{channel_type}}" ,
"members" : {
"$in" : "{{users}}"
}
}
At query time, provide the actual values via filter_values:
{
"predefined_filter" : "user_messaging_channels" ,
"filter_values" : {
"channel_type" : "messaging" ,
"users" : [ "user123" , "user456" ]
}
}
You can also use placeholders in sort field names. Provide these values via sort_values:
{
"predefined_filter" : "team_channels" ,
"filter_values" : {
"team_id" : "engineering"
},
"sort_values" : {
"sort_field" : "last_message_at"
}
}
The Dashboard displays performance analysis for your Predefined Filters. Performance scores and recommendations become available once a filter receives significant traffic or exhibits notable latency. Not all filters will show analysis immediately—the system needs sufficient usage data to provide meaningful insights.
QueryChannels performance depends on your filter complexity and the volume of data. Understanding which fields perform well helps you build efficient queries.
These fields are indexed and perform efficiently at scale:
cid (full channel ID)
type
members
created_at
last_message_at
last_updated
These fields may have performance implications at scale:
member_count: Can be slow for large datasets
frozen: Limited index support
Complex nested queries : Multiple $and/$or combinations
Simple, selective filters perform better than complex queries:
Python Ruby PHP Go C# Java
# RECOMMENDED: Simple, selective filter with indexed fields
filter = {
"type" : "messaging" ,
"members" : { "$in" : [user_id]},
"last_message_at" : { "$gte" : thirty_days_ago},
} require 'getstream_ruby'
# RECOMMENDED: Simple, selective filter with indexed fields
filter = {
'type' => 'messaging' ,
'members' => { '$in' => [user_id] },
'last_message_at' => { '$gte' => thirty_days_ago }
} // RECOMMENDED: Simple, selective filter with indexed fields
$filter = ( object )[
"type" => "messaging" ,
"members" => ( object )[ '$in' => [$userId]],
"last_message_at" => ( object )[ '$gte' => $thirtyDaysAgo],
]; // RECOMMENDED: Simple, selective filter with indexed fields
filter := map [ string ] any {
"type" : "messaging" ,
"members" : map [ string ] any {
"$in" : [] string {userID},
},
"last_message_at" : map [ string ] any {
"$gte" : thirtyDaysAgo,
},
} // RECOMMENDED: Simple, selective filter with indexed fields
var filter = new Dictionary < string , object >
{
[ "type" ] = "messaging" ,
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { userId } },
[ "last_message_at" ] = new Dictionary < string , object > { [ "$gte" ] = thirtyDaysAgo }
}; // RECOMMENDED: Simple, selective filter with indexed fields
Map< String , Object > filter = Map. of (
"type" , "messaging" ,
"members" , Map. of ( "$in" , List. of (userId)),
"last_message_at" , Map. of ( "$gte" , thirtyDaysAgo));
The most efficient sort fields are:
last_message_at
last_updated
created_at
For consistent and efficient pagination:
Use reasonable limits : The default limit is 10 and max is 30. Larger page sizes increase response time and payload size.
Include a members filter : Always include members: { $in: [userID] } in your filter for consistent pagination. Without this, channel list changes during pagination can cause channels to be skipped or duplicated.
Respect the offset maximum : The maximum offset is 1000. For datasets larger than this, use time-based filtering (e.g., last_message_at or created_at) to paginate through older data.
Python Ruby PHP Go C# Java
from getstream.models import SortParamRequest
# Efficient pagination with members filter
channels = client.chat.query_channels(
filter_conditions = { "type" : "messaging" , "members" : { "$in" : [user_id]}},
sort = [SortParamRequest( field = "last_message_at" , direction =- 1 )],
limit = 20 ,
) require 'getstream_ruby'
Models = GetStream :: Generated :: Models
# Efficient pagination with members filter
channels = client. chat . query_channels (
Models :: QueryChannelsRequest . new (
filter_conditions: { 'type' => 'messaging' , 'members' => { '$in' => [user_id] } },
sort: [ Models :: SortParamRequest . new ( field: 'last_message_at' , direction: - 1 )],
limit: 20
)
) use GetStream\ChatClient ;
use GetStream\GeneratedModels as Models ;
// Efficient pagination with members filter
$response = $client -> queryChannels ( new Models\QueryChannelsRequest (
filterConditions : ( object )[
"type" => "messaging" ,
"members" => ( object )[ '$in' => [$userId]],
],
sort : [ new Models\SortParamRequest ( field : "last_message_at" , direction : - 1 )],
limit : 20 ,
)); // Efficient pagination with members filter
resp, err := client. Chat (). QueryChannels (ctx, & getstream . QueryChannelsRequest {
FilterConditions: map [ string ] any {
"type" : "messaging" ,
"members" : map [ string ] any {
"$in" : [] string {userID},
},
},
Sort: [] getstream . SortParamRequest {{Field: getstream. PtrTo ( "last_message_at" ), Direction: getstream. PtrTo ( - 1 )}},
Limit: getstream. PtrTo ( 20 ),
}) // Efficient pagination with members filter
var resp = await chat. QueryChannelsAsync ( new QueryChannelsRequest
{
FilterConditions = new Dictionary < string , object >
{
[ "type" ] = "messaging" ,
[ "members" ] = new Dictionary < string , object > { [ "$in" ] = new [] { userId } }
},
Sort = new List < SortParamRequest >
{
new SortParamRequest { Field = "last_message_at" , Direction = - 1 }
},
Limit = 20
}); // Backend SDK: Efficient pagination with members filter
chat. queryChannels (QueryChannelsRequest. builder ()
. filterConditions (Map. of (
"type" , "messaging" ,
"members" , Map. of ( "$in" , List. of (userId))))
. sort (List. of (SortParamRequest. builder (). field ( "last_message_at" ). direction ( - 1 ). build ()))
. limit ( 20 )
. build ()). execute ();
Use Predefined Filters for frequently used query patterns in production
Filter by indexed fields (cid, type, members, last_message_at, created_at)
Add time-based filters to limit the scan scope (e.g., last_message_at within last 30 days)
Avoid deep nesting of $and/$or operators
Monitor performance through the Dashboard when using Predefined Filters