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:
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 }}
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});
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);
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.
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.
val filter = Filters.and( Filters.eq("type", "messaging"), Filters.`in`("members", listOf("thierry")),)
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.
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});
// Android SDK// Get the first 10 channelsFilterObject 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.data(); } else { // Handle result.error() }});// Get the second 10 channelsint nextOffset = 10; // Skips first 10QueryChannelsRequest nextRequest = new QueryChannelsRequest(filter, nextOffset, limit, sort, messageLimit, memberLimit);client.queryChannels(nextRequest).enqueue(result -> { if (result.isSuccess()) { List<Channel> channels = result.data(); } else { // Handle result.error() }});// Backend SDK// Get first 10 channelsMap<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 channelsvar nextChannels = chat.queryChannels(QueryChannelsRequest.builder() .filterConditions(filter) .state(true) .limit(10) .offset(10) .build()).execute();
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 skipvar channels = await Client.QueryChannelsAsync(filters, limit: 30, offset: 60);
Always include members: { $in: [userID] } in your filter to ensure consistent pagination results. Without this filter, channel list changes may cause pagination issues.
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
// Most performant: Filter by CIDconst filter = { cid: channelCID };// Recommended for social messagingconst filter = { type: "messaging", members: { $in: [userID] } };// Not recommended: type aloneconst filter = { type: "messaging" };
// Most performant: Filter by CIDval filter = Filters.eq("cid", channelCid)// Recommended for social messagingval filter = Filters.and( Filters.eq("type", "messaging"), Filters.`in`("members", listOf(userId)),)// Not recommended: type aloneval filter = Filters.eq("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.
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.
Good Pattern: Selective Filter with Indexed Fields
Use indexed fields like type, members, and last_message_at for efficient queries:
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.
Create and manage Predefined Filters in the Stream Dashboard. Navigate to your app's settings to define filter templates with placeholders for dynamic values.
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.
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.