Querying Channels

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:

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
    }
  }
}

Query Parameters

NameTypeDescriptionDefaultOptional
filtersobjectFilter criteria for channel fields. See Queryable Fields for available options.{}
sortobject or array of objectsSorting 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}]
optionsobjectAdditional 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.

Common Filters

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.

Messaging and Team Channels

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.

let currentUserChannels = chatClient.channelListController(
  query: .init(
    filter: .and([
      .equal(.type, to: .messaging),
      .containMembers(userIds: [chatClient.currentUserId!])
    ])
  )
)

currentUserChannels.synchronize()

Channel Queryable Built-In Fields

The following fields can be used in your filter criteria:

NameTypeDescriptionSupported OperatorsExample
frozenbooleanChannel frozen status$eqfalse
typestring or list of stringChannel type$in, $eqmessaging
cidstring or list of stringFull channel ID (type:id)$in, $eqmessaging:general
membersstring or list of stringUser IDs of channel members$in, $eqmarcelo or [thierry, marcelo]
invitestring (pending, accepted, rejected)Invite status$eqpending
joinedbooleanWhether the current user has joined the channel$eqtrue
mutedbooleanWhether the current user has muted the channel$eqtrue
member.user.namestringName property of a channel member$autocomplete, $eqmarc
created_by_idstringID of the user who created the channel$eqmarcelo
hiddenbooleanWhether the current user has hidden the channel$eqfalse
last_message_atstring (RFC3339 timestamp)Time of the last message$eq, $gt, $lt, $gte, $lte, $exists2021-01-15T09:30:20.45Z
member_countintegerNumber of members$eq, $gt, $lt, $gte, $lte5
created_atstring (RFC3339 timestamp)Channel creation time$eq, $gt, $lt, $gte, $lte, $exists2021-01-15T09:30:20.45Z
updated_atstring (RFC3339 timestamp)Channel update time$eq, $gt, $lt, $gte, $lte2021-01-15T09:30:20.45Z
teamstringTeam associated with the channel$eqstream
last_updatedstring (RFC3339 timestamp)Time of last message, or channel creation time if no messages exist$eq, $gt, $lt, $gte, $lte2021-01-15T09:30:20.45Z
disabledbooleanChannel disabled status$eqfalse
has_unreadbooleanWhether the user has unread messages (only true supported, max 100 channels)truetrue
app_bannedstringFilter by application-banned users (only for 2-member channels)excluded, onlyexcluded

For supported query operators, see Query Syntax Operators.

The app_banned filter only works on direct message channels with exactly 2 members.

Query Options

NameTypeDescriptionDefaultOptional
statebooleanReturn channel statetrue
watchbooleanSubscribe to real-time updates for returned channelstrue
limitintegerNumber of channels to return (max 30)10
offsetintegerNumber of channels to skip (max 1000)0
message_limitintegerMessages to include per channel (max 300)25
member_limitintegerMembers 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.

Response

The API returns a list of ChannelState objects containing all information needed to render channels without additional API calls.

ChannelState Fields

Field NameDescription
channelChannel data
messagesRecent messages (based on message_limit)
watcher_countNumber of users currently watching
readRead state for up to 100 members, ordered by most recently added (current user's read state is always included)
membersUp to 100 members, ordered by most recently added
pinned_messagesUp to 10 most recent pinned messages

Pagination

Use limit and offset to paginate through results:

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)
    }
  }
}

Always include members: { $in: [userID] } in your filter to ensure consistent pagination results. Without this filter, channel list changes may cause pagination issues.

Best Practices

Channel Creation and Watching

A channel is not created in the API until one of the following methods is called. Each method has subtle differences:

channel.create();
channel.query();
channel.watch();

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.

Filter Best Practices

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 CID
const filter = { cid: channelCID };

// Recommended for social messaging
const filter = { type: "messaging", members: { $in: [userID] } };

// Not recommended: type alone
const filter = { 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.

Sort Best Practices

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
const sort = { last_message_at: -1 };

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.

Good Pattern: Selective Filter with Indexed Fields

Use indexed fields like type, members, and last_message_at for efficient queries:

// ✅ 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 });

Bad Pattern: Overly Broad or Complex Filters

Avoid overly broad filters or deep nesting of logical operators, which can cause performance issues at scale and may result in dynamic rate limiting:

// ❌ 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 }],
    },
  ],
};

Using Predefined Filters in Production

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
// 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,
  },
);

Monitoring Query Performance

Use the Stream Dashboard to monitor and optimize your QueryChannels performance:

  1. Create Predefined Filters for your frequently used query patterns
  2. View Performance Analysis in the Dashboard once filters receive traffic
  3. Review Recommendations for optimization opportunities
  4. 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

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.

Why Use Predefined Filters

  • 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)

Creating Predefined Filters

Create and manage Predefined Filters in the Stream Dashboard. Navigate to your app's settings to define filter templates with placeholders for dynamic values.

Using Predefined Filters

Reference a predefined filter by name and provide values for any placeholders:

resp, err := c.QueryChannels(ctx, &QueryOption{
	PredefinedFilter: "user_messaging_channels",
	FilterValues: map[string]interface{}{
		"user_id": "user123",
	},
	Limit: 20,
})

Placeholder Syntax

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"
  }
}

Performance Analysis

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.

Performance Considerations

QueryChannels performance depends on your filter complexity and the volume of data. Understanding which fields perform well helps you build efficient queries.

Well-Optimized Fields

These fields are indexed and perform efficiently at scale:

  • cid (full channel ID)
  • type
  • members
  • created_at
  • last_message_at
  • last_updated

Fields to Use with Caution

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

Query Complexity

Simple, selective filters perform better than complex queries:

// RECOMMENDED: Simple, selective filter with indexed fields
filter := map[string]interface{}{
	"type": "messaging",
	"members": map[string]interface{}{
		"$in": []string{userID},
	},
	"last_message_at": map[string]interface{}{
		"$gte": thirtyDaysAgo,
	},
}

Sort Performance

The most efficient sort fields are:

  • last_message_at
  • last_updated
  • created_at

Pagination Best Practices

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.
// Efficient pagination with members filter
resp, err := c.QueryChannels(ctx, &QueryOption{
	Filter: map[string]interface{}{
		"type": "messaging",
		"members": map[string]interface{}{
			"$in": []string{userID},
		},
	},
	Sort:  []*SortOption{{Field: "last_message_at", Direction: -1}},
	Limit: 20,
})

Recommendations

  1. Use Predefined Filters for frequently used query patterns in production
  2. Filter by indexed fields (cid, type, members, last_message_at, created_at)
  3. Add time-based filters to limit the scan scope (e.g., last_message_at within last 30 days)
  4. Avoid deep nesting of $and/$or operators
  5. Monitor performance through the Dashboard when using Predefined Filters