Applies to: Chat | All SDKs
Error codes covered: 4, 48, 70
Quick Answer (2 minutes)
If your channel query is failing or returning unexpected results, check these four things first:
-
Always include
{ members: { $in: [userId] } }in client-side queries. Without this, you will get error code 70. -
Sort by
last_message_atfor best performance. This field is indexed and optimized for channel list queries. -
Use pagination (
limit+offset). Never attempt to load all channels in a single request. - Error code 70 = permission mismatch, almost always caused by a missing member filter. Error code 48 = request timeout, your query is too broad. Error code 4 = invalid filter syntax or unsupported field.
The #1 Mistake: Missing Member Filter (Error Code 70)
The error
StreamChat error code 70: QueryChannels failed with error:
"N channels match your query but cannot be returned because you don't
have access to them. Did you forget to include {members: $in: [user_id]}?"Why this happens
Client-side requests (made with a user token) have permission checks applied automatically. When you query channels without filtering by membership, the query may find channels the user is not a member of. The server cannot return those channels because the user lacks permission to view them, resulting in error code 70 (HTTP 403).
The fix is straightforward: always include a members filter in your client-side channel queries.
Fix for every SDK
JavaScript / Node.js (client-side)
const filter = {
type: 'messaging',
members: { $in: [currentUserId] }
};
const sort = [{ last_message_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });iOS (Swift)
let filter = Filter<ChannelListFilterScope>.and([ .equal(.type, .messaging), .containMembers(userIds: [currentUserId]) ]) let sort = [Sorting<ChannelListSortingKey>(.lastMessageAt, isAscending: false)] let controller = chatClient.channelListController(query: .init( filter: filter, sort: sort, pageSize: 20 ))
Android (Kotlin)
val filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf(currentUserId))
)
val sort = QuerySortByField.descByName<Channel>("last_message_at")
val request = QueryChannelsRequest(
filter = filter,
querySort = sort,
limit = 20
)Flutter (Dart)
final filter = Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', [currentUserId]),
]);
final sort = [SortOption('last_message_at', direction: SortOption.DESC)];
final channels = await client.queryChannels(
filter: filter,
sort: sort,
paginationParams: PaginationParams(limit: 20),
);Understanding Filter Operators
Channel queries accept a filter object that supports the following operators:
| Operator | Meaning | Example |
|---|---|---|
$eq |
Equals |
{ type: { $eq: 'messaging' } } or shorthand { type: 'messaging' }
|
$ne |
Not equals | { type: { $ne: 'livestream' } } |
$gt |
Greater than | { member_count: { $gt: 5 } } |
$gte |
Greater than or equal | { member_count: { $gte: 2 } } |
$lt |
Less than | { member_count: { $lt: 100 } } |
$lte |
Less than or equal | { member_count: { $lte: 50 } } |
$in |
Value is in array | { members: { $in: ['user1', 'user2'] } } |
$nin |
Value is not in array | { cid: { $nin: ['messaging:hidden'] } } |
$and |
All conditions must match | { $and: [{ type: 'messaging' }, { frozen: false }] } |
$or |
Any condition can match | { $or: [{ type: 'messaging' }, { type: 'team' }] } |
$exists |
Field exists (true) or does not exist (false) | { last_message_at: { $exists: true } } |
$autocomplete |
Prefix search (uses full-text search index) |
{ name: { $autocomplete: 'gen' } } matches “general” |
Notes on the members filter
-
{ members: { $in: ['user1'] } }— returns channels whereuser1is a member (among other members). -
{ members: { $eq: ['user1', 'user2'] } }— returns channels with exactly these two members and no others. This is the correct way to find a 1-on-1 DM channel. -
{ members: { $nin: ['user1'] } }— returns channels whereuser1is not a member.
Sorting Best Practices
| Sort Field | Direction | Performance | Notes |
|---|---|---|---|
last_message_at |
-1 (descending) |
Best | Indexed. Most recent activity first. Recommended for most channel lists. |
created_at |
-1 (descending) |
Good | Indexed. Useful when you want newest channels first regardless of activity. |
updated_at |
-1 (descending) |
Good | Indexed. Captures all updates including metadata changes, not just messages. |
member_count |
-1 or 1
|
Moderate | Useful for finding the most or least populated channels. |
has_unread / unread_count
|
-1 |
Moderate | Client-side only. Computed at query time from read state (not indexable). |
| Custom fields |
-1 or 1
|
Slower | Supported but may not be indexed. Use sparingly. |
NULLS LAST behavior: When sorting by last_message_at, channels that have never had a message (where last_message_at is null) appear at the end of the results. This is the expected database behavior and matches most UI expectations.
Tip: To avoid seeing empty channels entirely, add { last_message_at: { $exists: true } } to your filter.
Pagination
Channel queries use limit and offset for pagination. The default limit is 10 and the maximum is 30 per request (configurable up to higher limits for specific apps). Never attempt to load all channels without pagination — this causes timeouts (error code 48) and degrades performance for everyone.
Basic offset pagination
// JavaScript — basic offset pagination
let offset = 0;
const limit = 20;
let hasMore = true;
while (hasMore) {
const channels = await client.queryChannels(
filter, sort, { limit, offset }
);
hasMore = channels.length === limit;
offset += limit;
// Process channels...
}Common pagination mistakes
- Not checking for empty results. If you do not check whether the result set is empty or smaller than the limit, your loop will run indefinitely, creating rapid-fire API calls that trigger rate limiting (HTTP 429).
- Setting limit too high. The maximum limit for channel queries is 30 by default. Setting a higher value will be capped at 30 or may return an error (code 4).
- Using very large offsets. Offset-based pagination becomes less efficient as the offset grows (the database still scans past the skipped rows). For apps with thousands of channels, keep offsets reasonable or implement cursor-based pagination using the last item’s sort value as the next page’s boundary.
Performance Optimization
-
Always filter by
type. Thelast_message_atindex is compound:(app_id, type, last_message_at). Includingtypein your filter allows the database to use this index efficiently. -
Filter out empty channels. Add
{ last_message_at: { $exists: true } }to skip channels with no messages. This reduces result set size and improves response times. -
Limit response data. Use
state: falseif you do not need the full channel state (members, reads, etc.). Usemessage_limit: 0if you do not need messages. Each of these reduces payload size and server processing time. -
Watch selectively. The
watch: trueoption subscribes you to real-time events for the queried channels. Only use this for channels currently visible on screen. Do not watch all channels across all pages — it consumes server resources and WebSocket bandwidth. -
Cache results client-side. Listen to WebSocket events (
message.new,channel.updated, etc.) to update your local channel list in real time instead of re-querying the API. The SDKs handle this automatically if you use the built-in channel list components.
Common Query Patterns
1. User’s channel list (most common)
Show all channels with messages, sorted by most recent activity:
const filter = {
type: 'messaging',
members: { $in: [userId] },
last_message_at: { $exists: true }
};
const sort = [{ last_message_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });2. Search channels by name
Use $autocomplete for prefix-based channel name search:
const filter = {
type: 'messaging',
members: { $in: [userId] },
name: { $autocomplete: searchQuery }
};
const sort = [{ last_message_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });3. Unread channels only
Filter to channels with unread messages (client-side only):
const filter = {
type: 'messaging',
members: { $in: [userId] },
has_unread: true
};
const sort = [{ last_message_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });4. Find a specific DM channel (exact member match)
Use $eq on members to find a channel with exactly these two members:
const filter = {
type: 'messaging',
members: { $eq: [userId, otherUserId] }
};
const channels = await client.queryChannels(filter);Note: $eq on members performs an exact match. The channel must have exactly these members and no others. The order of user IDs does not matter.
5. Archived channels
Query channels that a user has archived (client-side only):
const filter = {
type: 'messaging',
members: { $in: [userId] },
archived: true
};
const sort = [{ last_message_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });6. Pinned channels
Query channels that a user has pinned (client-side only):
const filter = {
type: 'messaging',
members: { $in: [userId] },
pinned: true
};
const sort = [{ pinned_at: -1 }];
const channels = await client.queryChannels(filter, sort, { limit: 20 });7. Server-side: All channels (admin)
Server-side queries bypass permission checks, so no member filter is needed:
// Server-side only — no member filter needed
const filter = { type: 'messaging' };
const sort = [{ last_message_at: -1 }];
const channels = await serverClient.queryChannels(filter, sort, { limit: 30 });Troubleshooting Slow Queries
Every API response includes a duration field that tells you how long the server spent processing your request. Use this to diagnose performance issues:
| Symptom | Likely Cause | Fix |
|---|---|---|
duration is low (under 100ms) but total request time is high |
Network latency between your client and the Stream API | Check your app’s region setting in the Dashboard. Use the edge endpoint or the region closest to your users. |
duration is high (over 500ms) |
Query is too broad or not using indexes efficiently | Add more filters (especially type), reduce the result set, and ensure you are sorting by an indexed field like last_message_at. |
| Error code 48 (Request Timeout) | The query exceeded the server-side timeout (default 3 seconds for database queries) | Narrow your filters. Add type and members filters. Reduce limit. Avoid very large offsets. |
| Error code 4 (Input Error) | Invalid filter syntax, unsupported field name, or unsupported operator for the given field | Check your filter object for typos. Verify the field supports the operator you are using (see the filter operators table). |
Tip: If your app has a large number of channels and you consistently see slow queries, consider increasing the client-side timeout to 6000ms to avoid premature timeouts. However, the proper fix is to optimize the query itself by adding appropriate filters.
Client-Side vs Server-Side Queries
| Client-Side | Server-Side | |
|---|---|---|
| Authentication | User token (JWT) | API key + secret |
| Member filter required? | YES — or you get error code 70 | No |
| Permissions applied? | Yes — results filtered by user access | Bypassed — returns all matching channels |
| Unread/read filters? | Supported (has_unread, unread_count) |
Not supported (error code 4) |
| Use case | User-facing channel list in your app | Admin dashboards, backend operations, migrations |
Dev vs Production Gotcha
This is one of the most common causes of “it works in development but breaks in production”:
- During development, you enable “Disable Auth Checks” and/or “Disable Permissions Checks” in the Stream Dashboard.
- With these settings enabled, client-side channel queries work without the
membersfilter because permission checks are bypassed. - You ship to production with these settings disabled (as they should be).
- The same query that worked in development now fails with error code 70.
Best practice: Always include the members: { $in: [userId] } filter in your client-side queries, even during development. This ensures your queries will work correctly when you move to production.
Diagnostics Checklist
If you need to escalate a channel query issue, copy this template, fill it out, and include it in your support request:
Channel Query Issue Diagnostics ================================ 1. Error code and message: 2. Client-side or server-side query? 3. Filter (paste the filter object): 4. Sort (paste the sort): 5. Options (limit, offset, state, watch, message_limit, etc.): 6. SDK + version: 7. "Disable Auth Checks" enabled? [ ] Yes [ ] No 8. "Disable Permissions Checks" enabled? [ ] Yes [ ] No 9. API response "duration" value: 10. Number of channels in your app (approx): 11. Does the query work server-side? [ ] Yes [ ] No [ ] Not tested 12. Environment: [ ] Development [ ] Production 13. Did this previously work? [ ] Yes [ ] No [ ] First time setting up 14. Any recent changes? (SDK upgrade, permission changes, new channel types):
Comments
0 comments
Please sign in to leave a comment.