State recovery

When the connection drops, the user can miss new messages, reactions, and other channel changes. State recovery is how the SDK catches up after it reconnects. It reloads the channels the user had open and updates local state so your UI can show current data.

You choose how recovery behaves with IStreamClientConfig.StateRecoveryStrategy. The default is ReplayEvents: missed changes are raised through the normal event handlers (MessageReceived, ReactionAdded, and so on), then channels are reloaded from the server and watched again.

Choose a strategy

Set the strategy on StreamClientConfig before you create the client.

using StreamChat.Core;
using StreamChat.Core.Configs;

var config = new StreamClientConfig
{
    StateRecoveryStrategy = StateRecoveryStrategy.ReplayEvents,
};
var client = StreamChatClient.CreateDefaultClient(config);
ValueWhat happensWhen to use it
ReplayEvents (default)The SDK sends missed changes through normal event handlers, then reloads and watches the channels again. StateRecovered is raised at the end.Your UI already updates from events.
BatchStateUpdateThe SDK updates local channel state without normal event handlers, then raises StateRecovered.Many missed events could make event replay too slow.
DisabledThe SDK does not recover channels. StateRecovered is not raised.Your app reloads the channels itself after reconnect.

Use ReplayEvents if your UI updates from event handlers such as MessageReceived or ReactionAdded. After a long disconnect on a busy livestream, replaying many events at once can make the UI pause for a moment.

Use BatchStateUpdate if that pause is a problem. Listen to StateRecovered and refresh the UI from channel state.

Use Disabled only if you restore state yourself.

Recovery pipeline

For ReplayEvents and BatchStateUpdate, recovery works like this:

  1. When the connection drops, the SDK saves the ids of channels that were being watched. WatchedChannels can be empty during recovery.
  2. The first ConnectUserAsync after login is a normal connect, not recovery. StateRecovered does not fire.
  3. On reconnect, the SDK applies changes that happened while you were disconnected, then reloads each channel from the server and starts watching it again.
  4. StateRecovered is raised once with the recovered channels and any channel ids that could not be recovered.

StateRecovered

Subscribe on the client. You need this for BatchStateUpdate because event handlers such as MessageReceived do not fire during recovery. It is also useful for ReplayEvents: reloading a channel can update channel.Messages without firing MessageReceived for every message.

client.StateRecovered += args =>
{
    foreach (var channel in args.Channels)
    {
        // Rebuild this channel's UI from channel.Messages and similar collections.
        Debug.Log($"Recovered {channel.Cid}, messages: {channel.Messages.Count}");
    }

    foreach (var cid in args.UnrecoveredChannelCids)
    {
        // This channel could not be recovered. Close or mark its UI; local data for it is out of date.
        Debug.Log($"Could not recover {cid}");
    }
};
  • Channels: channels that were reloaded and watched again. Collections such as Messages are up to date when the event is raised.
  • UnrecoveredChannelCids: channel ids (type and id, for example messaging:lobby) that could not be recovered. This can happen if a channel was deleted, the user lost access, or loading failed. Use this to close or mark the related UI.
  • IsComplete: true when every channel the SDK tried to recover succeeded. Channels beyond the 100-channel recovery limit are not listed in UnrecoveredChannelCids.

Recovery adds the newest messages to what was already loaded. A long disconnect can leave a gap in Messages: older loaded messages, then a jump to the newest messages. LoadOlderMessagesAsync cannot fill that gap, because it loads messages older than the oldest message already in the list.

Batch state update

BatchStateUpdate uses the same recovery flow as ReplayEvents, but missed events update local state without raising normal event handlers. Refresh the UI from StateRecovered.

Some events are still raised because they are not stored on a channel:

  • CustomEventReceived
  • ChannelDeleted
  • membership and invite notifications (AddedToChannelAsMember, ChannelInviteReceived, and similar)

Do not expect MessageReceived or ReactionAdded during BatchStateUpdate recovery.

var config = new StreamClientConfig
{
    StateRecoveryStrategy = StateRecoveryStrategy.BatchStateUpdate,
};
var client = StreamChatClient.CreateDefaultClient(config);

client.StateRecovered += args =>
{
    foreach (var channel in args.Channels)
    {
        Debug.Log($"Recovered {channel.Cid}, messages: {channel.Messages.Count}");
    }

    foreach (var cid in args.UnrecoveredChannelCids)
    {
        Debug.Log($"Could not recover {cid}");
    }
};

Limits

If the user watches many channels or stays disconnected for a long time:

  • The SDK recovers at most 100 channels (the ones with the most recent activity). Extra channels keep old local state and are not watched again. A warning is logged.
  • If the SDK has no timestamp for the last event, or the disconnect lasted more than 30 days, missed events are not fetched. The SDK still reloads and watches the channels.
  • If too many events accumulated, the server rejects the event fetch. The SDK still reloads channel state from a query.
  • If recovery fails for one channel, the others still recover.
  • If the connection drops again during recovery, that recovery is discarded and a new one starts.

Disabled

The SDK does not restore state, does not start watching again, and does not raise StateRecovered. Local state and WatchedChannels stay as they were before the disconnect.

When ConnectionState is Connected again, query the channels your app needs. That call refreshes state and starts watching.

Do not call WatchAsync to recover. IStreamChannel.IsWatched can still be true from before the disconnect, so WatchAsync does nothing.

using System.Linq;
using StreamChat.Core;
using StreamChat.Core.Configs;
using StreamChat.Core.QueryBuilders.Filters.Channels;

var config = new StreamClientConfig
{
    StateRecoveryStrategy = StateRecoveryStrategy.Disabled,
};
var client = StreamChatClient.CreateDefaultClient(config);

client.Connected += async _ =>
{
    // With Disabled, WatchedChannels is not cleared on disconnect, so you can read it here.
    var cids = client.WatchedChannels.Select(channel => channel.Cid).ToArray();
    if (cids.Length == 0)
    {
        return;
    }

    // Do not call WatchAsync: IsWatched is still true, so it does nothing.
    await client.QueryChannelsAsync(new[] { ChannelFilter.Cid.In(cids) }, limit: 30);
};

If you save channel ids yourself, do it before the disconnect. With Disabled, you can also read WatchedChannels after Connected because the list is not cleared.

See Querying Channels for filters, sort, and pagination.