# Presence Presence enables clients to be aware of other clients that are currently "present" on a channel. Each member present on a channel has a unique self-assigned client identifier and system-assigned connection identifier, along with an optional payload that can be used to describe the member's status or attributes. Presence enables you to quickly build apps such as chat rooms and multiplayer games by automatically keeping track of who is present in real time across any device. Other devices and services can subscribe to presence events in real time using the realtime interface or with [integrations](https://ably.com/docs/platform/integrations). You can also request a list of clients or devices on a channel at a particular point in time with the REST interface. ## Trigger presence events Whenever a member enters or leaves a channel, or updates their member data, a presence event is emitted to all presence subscribers on that channel. The following presence events are emitted: | Event | Description | |-------|-------------| | Enter | A new member has entered the channel | | Leave | A member who was present has now left the channel. This may be a result of an explicit request to leave or implicitly when detaching from the channel. Alternatively, if a member's connection is abruptly disconnected and they do not resume their connection within a minute, Ably treats this as a leave event as the client is no longer present | | Update | An already present member has updated their [member data](#member-data). Being notified of member data updates can be useful, for example, it can be used to update the status of a user when they are typing a message | | Present | When subscribing to presence events on a channel that already has members present, this event is emitted for every member already present on the channel before the subscribe listener was registered | ### Member data In addition to the [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) for members on a channel, it is also possible to include data when entering a channel. Clients can [update](https://ably.com/docs/api/realtime-sdk/presence#update) their data at any point which will be broadcasted to all presence subscribers as an `update` event. The following is an example of subscribing to enter and leave events, then entering the presence set and updating a client's member data: ```realtime_javascript /* Subscribe to presence enter events */ await channel.presence.subscribe('enter', (member) => { console.log(member.data); // => not moving }); /* Subscribe to presence update events */ await channel.presence.subscribe('update', (member) => { console.log(member.data); // => travelling North }); /* Enter this client with data and update once entered */ await channel.presence.enter('not moving'); await channel.presence.update('travelling North'); ``` ```realtime_nodejs /* Subscribe to presence enter events */ await channel.presence.subscribe('enter', (member) => { console.log(member.data); // => not moving }); /* Subscribe to presence update events */ await channel.presence.subscribe('update', (member) => { console.log(member.data); // => travelling North }); /* Enter this client with data and update once entered */ await channel.presence.enter('not moving'); await channel.presence.update('travelling North'); ``` ```realtime_java /* Subscribe to presence enter and update events */ channel.presence.subscribe(new Presence.PresenceListener() { @Override public void onPresenceMessage(PresenceMessage member) { switch (member.action) { case enter: { System.out.println("ENTER: " + member.data); // => "not moving" break; } case update: { System.out.println("UPDATE: " + member.data); // => "travelling North" break; } } } }); /* Enter this client with data and update once entered */ channel.presence.enter("not moving", new CompletionListener() { @Override public void onSuccess() { System.out.println("Entered channel successfully with initial data"); try { channel.presence.update("travelling North", new CompletionListener() { @Override public void onSuccess() { System.out.println("Updated presence data successfully"); } @Override public void onError(ErrorInfo errorInfo) { System.err.println("Failed to update presence data: " + errorInfo.message); } }); } catch (AblyException e) { throw new RuntimeException(e); } } @Override public void onError(ErrorInfo errorInfo) { System.err.println("Failed to enter channel: " + errorInfo.message); } }); ``` ```realtime_csharp /* Subscribe to presence enter and update events */ channel.Presence.Subscribe(member => { switch (member.Action) { case PresenceAction.Enter: case PresenceAction.Update: { Console.WriteLine(member.Data); // => travelling North break; } } }); /* Enter this client with data and update once entered */ await channel.Presence.EnterAsync("not moving"); await channel.Presence.UpdateAsync("travelling North"); ``` ```realtime_ruby # Subscribe to presence enter events channel.presence.subscribe(:enter) do |member| puts member.data # => not moving end # Subscribe to presence update events channel.presence.subscribe(:update) do |member| puts member.data # => travelling North end # Enter this client with data and update once entered channel.presence.enter(withData: 'not moving') do channel.presence.update(withData: 'travelling North') end ``` ```realtime_objc // Subscribe to presence enter events [channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) { NSLog(@"%@", member.data); // prints "not moving" }]; // Subscribe to presence update events [channel.presence subscribe:ARTPresenceUpdate callback:^(ARTPresenceMessage *member) { NSLog(@"%@", member.data); // prints "travelling North" }]; // Enter this client with data and update once entered [channel.presence enter:@"not moving" callback:^(ARTErrorInfo *error) { [channel.presence update:@"travelling North"]; }]; ``` ```realtime_swift // Subscribe to presence enter events channel.presence.subscribe(.enter) { member in print(member.data) // prints "not moving" } // Subscribe to presence update events channel.presence.subscribe(.update) { member in print(member.data) // prints "travelling North" } // Enter this client with data and update once entered channel.presence.enter("not moving") { error in channel.presence.update("travelling North") } ``` ```realtime_go /* Subscribe to presence enter events */ channel.Presence.Subscribe( context.Background(), ably.PresenceActionEnter, func(member *ably.PresenceMessage) { log.Printf("%v", member.Data) // => not moving }) /* Subscribe to presence update events */ channel.Presence.Subscribe( context.Background(), ably.PresenceActionUpdate, func(member *ably.PresenceMessage) { log.Printf("%v", member.Data) // => travelling North }) /* Enter this client with data and update once entered */ channel.Presence.Enter(context.Background(), "not moving") channel.Presence.Update(context.Background(), "travelling North") ``` ```realtime_flutter channel .presence .subscribe(action: PresenceAction.enter) .listen((message) { print(message.data); // => not moving }); // Subscribe to presence update events channel .presence .subscribe(action: PresenceAction.update) .listen((message) { print(message.data); // => travelling North }); // Enter this client with data and update once entered await channel.presence.enter('not moving'); await channel.presence.update('travelling North'); ``` ### Managing multiple client IDs An Ably client instance might, if on an application server for example, publish messages and be present on channels on behalf of multiple distinct [client IDs](https://ably.com/docs/api/realtime-sdk#client-id). The channel's `Presence` object therefore also supports methods that enable presence messages to be emitted for a [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) specified at the time of the call, rather than implicitly based on the [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) [specified when the SDK is instantiated or authenticated](https://ably.com/docs/api/realtime-sdk#client-id). Each unique [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) may only be present once when entering on behalf of another client as the unique identifier for each member in a presence set is the combined [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) and shared [`connectionId`](https://ably.com/docs/api/realtime-sdk/connection#id). A single [`clientId`](https://ably.com/docs/api/realtime-sdk#client-id) may be present multiple times on the same channel via different client connections. Ably considers these as different members of the presence set for the channel, however they will be differentiated by their unique connectionId. For example, if a client with ID “Sarah” is connected to a chat channel on both a desktop and a mobile device simultaneously, “Sarah” will be present twice in the presence member set with the same client ID, yet will have two unique connection IDs. A member of the presence set is therefore unique by the combination of the clientId and connectionId strings. In order to be able to publish presence changes for arbitrary client IDs, the SDK must have been instantiated either with an [API key](https://faqs.ably.com/what-is-an-app-api-key), or with a [token bound to a wildcard client ID](https://faqs.ably.com/can-a-client-emulate-any-client-id-i.e.-authenticate-using-a-wildcard-client-id). ```rest_javascript const rest = new Ably.Rest({ key: 'your-api-key' }); /* request a wildcard token */ const token = await rest.auth.requestToken({ clientId: '*' }); const realtime = new Ably.Realtime({ token: token }); const channel = realtime.channels.get('realtime-chat'); await channel.presence.subscribe('enter', (member) => { console.log(member.clientId + ' entered realtime-chat'); }); await channel.presence.enterClient('Bob'); // => Bob entered realtime-chat await channel.presence.enterClient('Mary'); // => Mary entered realtime-chat ``` ```rest_nodejs const rest = new Ably.Rest({ key: 'your-api-key' }); /* request a wildcard token */ const token = await rest.auth.requestToken({ clientId: '*' }); const realtime = new Ably.Realtime({ token: token }); const channel = realtime.channels.get('realtime-chat'); await channel.presence.subscribe('enter', (member) => { console.log(member.clientId + ' entered realtime-chat'); }); await channel.presence.enterClient('Bob'); // => Bob entered realtime-chat await channel.presence.enterClient('Mary'); // => Mary entered realtime-chat ``` ```rest_ruby rest = Ably::Rest.new(key: 'your-api-key') # request a wildcard token rest.auth.requestToken(clientId: '*') do |token| realtime = Ably::Realtime.new(token: token) channel = realtime.channels.get('realtime-chat') channel.presence.subscribe(:enter) do |member| puts "#{member.client_id} entered realtime-chat" end channel.presence.enter_client 'Bob' # => Bob entered realtime-chat channel.presence.enter_client 'Mary' # => Mary entered realtime-chat end ``` ```rest_java /* request a wildcard token */ AblyRest rest = new AblyRest("your-api-key"); Auth.TokenParams params = new Auth.TokenParams(); params.clientId = "*"; ClientOptions options = new ClientOptions(); options.tokenDetails = rest.auth.requestToken(params, null); AblyRealtime realtime = new AblyRealtime(options); Channel channel = realtime.channels.get("realtime-chat"); channel.presence.subscribe(new Presence.PresenceListener() { @Override public void onPresenceMessage(PresenceMessage member) { System.out.println(member.clientId + " entered realtime-chat"); } }); CompletionListener noop = new CompletionListener() { @Override public void onSuccess() { System.out.println("Operation completed successfully."); } @Override public void onError(ErrorInfo errorInfo) { System.err.println("Operation failed with error: " + errorInfo.message); } }; channel.presence.enterClient("Bob", "message", noop); /* => Bob entered realtime-chat */ channel.presence.enterClient("Mary", "message", noop); /* => Mary entered realtime-chat */ ``` ```rest_csharp /* request a wildcard token */ AblyRest rest = new AblyRest("your-api-key"); TokenParams tokenParams = new TokenParams() { ClientId = "*"}; ClientOptions options = new ClientOptions(); options.TokenDetails = await rest.Auth.RequestTokenAsync(tokenParams, null); AblyRealtime realtime = new AblyRealtime(options); IRealtimeChannel channel = realtime.Channels.Get("realtime-chat"); channel.Presence.Subscribe(member => { Console.WriteLine(member.ClientId + " entered realtime-chat"); }); await channel.Presence.EnterClientAsync("Bob", null); /* => Bob entered realtime-chat */ await channel.Presence.EnterClientAsync("Mary", null); /* => Mary entered realtime-chat */ ``` ```rest_objc ARTRest* rest = [[ARTRest alloc] initWithKey:@"your-api-key"]; // request a wildcard token ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"*"]; [rest.auth requestToken:tokenParams withOptions:nil callback:^(ARTTokenDetails *tokenDetails, NSError *error) { ARTRealtime *realtime = [[ARTRealtime alloc] initWithToken:tokenDetails.token]; ARTRealtimeChannel *channel = [realtime.channels get:@"realtime-chat"]; [channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) { NSLog(@"%@ entered realtime-chat", member.clientId); }]; [channel.presence enterClient:@"Bob" data:nil]; // prints 'Bob entered realtime-chat' [channel.presence enterClient:@"Mary" data:nil]; // prints 'Mary entered realtime-chat' }]; ``` ```rest_swift let rest = ARTRest(key: "your-api-key") // request a wildcard token rest.auth.requestToken(ARTTokenParams(clientId: "*"), withOptions: nil) { tokenDetails, error in let realtime = ARTRealtime(token: tokenDetails!.token) let channel = realtime.channels.get("realtime-chat") channel.presence.subscribe(.enter) { member in print("\(member.clientId) entered realtime-chat") } channel.presence.enterClient("Bob", data: nil) // prints 'Bob entered realtime-chat' channel.presence.enterClient("Mary", data: nil) // prints 'Mary entered realtime-chat' } ``` ```rest_go rest, err := ably.NewREST( ably.WithKey("your-api-key")) if err != nil { log.Fatal(err) } // Request a wildcard token tokenParams := &ably.TokenParams{ClientID: "*"} tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams) if err != nil { log.Fatal(err) } // Initialize the Realtime client using the token realtime, err := ably.NewRealtime(ably.WithToken(tokenDetails.Token)) if err != nil { log.Fatal(err) } // Get the channel channel := realtime.Channels.Get("realtime-chat") // Subscribe to presence 'enter' events _, err = channel.Presence.Subscribe(context.Background(), ably.PresenceActionEnter, func(msg *ably.PresenceMessage) { log.Printf("%s entered realtime-chat", msg.ClientID) }) if err != nil { log.Fatal(err) } // Enter clients into the channel err = channel.Presence.EnterClient(context.Background(), "Bob", nil) if err != nil { log.Fatal(err) } err = channel.Presence.EnterClient(context.Background(), "Mary", nil) if err != nil { log.Fatal(err) } ``` ```rest_flutter final clientOptions = ably.ClientOptions( key: 'your-api-key', ); final rest = ably.Rest(options: clientOptions); // Request a wildcard token final tokenRequest = await rest.auth.requestToken( tokenParams: const ably.TokenParams(clientId: '*'), ); final realtime = ably.Realtime(options: ably.ClientOptions(key: tokenRequest.token)); final channel = realtime.channels.get('realtime-chat'); channel .presence .subscribe(action: PresenceAction.enter) .listen((message) { print('${message.clientId} entered realtime-chat'); }); await channel.presence.enterClient('Bob', 'Bob entered realtime-chat'); await channel.presence.enterClient('Mary', 'Mary entered realtime-chat'); ``` ## Subscribe to presence events Subscribe to a channel's presence set in order to receive presence events, by registering a listener. Presence events are emitted whenever a member enters or leaves the presence set, or updates their member data. Subscribing is an operation available to the realtime interface of an Ably SDK and uses the [`subscribe()`](https://ably.com/docs/api/realtime-sdk/presence#subscribe) method on the `Presence` object of a channel. The following is an example of subscribing to presence events and entering the presence set: ```realtime_javascript const realtime = new Ably.Realtime({ key: 'your-api-key', clientId: 'bob' }); const channel = realtime.channels.get('your-channel-name'); await channel.presence.subscribe('enter', (member) => { alert('Member ' + member.clientId + ' entered'); }); await channel.presence.enter(); ``` ```realtime_nodejs const realtime = new Ably.Realtime({ key: 'your-api-key', clientId: 'bob' }); const channel = realtime.channels.get('your-channel-name'); await channel.presence.subscribe('enter', (member) => { console.log('Member ' + member.clientId + ' entered'); }); await channel.presence.enter(); ``` ```realtime_ruby realtime = Ably::Realtime.new(key: 'your-api-key', client_id: 'bob') channel = realtime.channels.get('your-channel-name') channel.presence.subscribe(:enter) do |member| puts "Member #{member.client_id} entered" end channel.presence.enter ``` ```realtime_java ClientOptions options = new ClientOptions("your-api-key"); options.clientId = "bob"; AblyRealtime realtime = new AblyRealtime(options); Channel channel = realtime.channels.get("your-channel-name"); channel.presence.subscribe(new Presence.PresenceListener() { @Override public void onPresenceMessage(PresenceMessage member) { System.out.println("Member " + member.clientId + " : " + member.action.toString()); } }); channel.presence.enter("", new CompletionListener() { @Override public void onSuccess() { System.out.println("Entered presence successfully."); } @Override public void onError(ErrorInfo errorInfo) { System.err.println("Failed to enter presence: " + errorInfo.message); } }); ``` ```realtime_csharp ClientOptions options = new ClientOptions("your-api-key") { ClientId = "bob"}; AblyRealtime realtime = new AblyRealtime(options); IRealtimeChannel channel = realtime.Channels.Get("your-channel-name"); channel.Presence.Subscribe(member => { Console.WriteLine("Member " + member.ClientId + " : " + member.Action); }); await channel.Presence.EnterAsync(null); ``` ```realtime_objc ARTClientOptions *options = [[ARTClientOptions alloc] initWithKey:@"your-api-key"]; options.clientId = @"bob"; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"your-channel-name"]; [channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) { NSLog(@"Member %@ entered", member.clientId); }]; [channel.presence enter:nil]; ``` ```realtime_swift let options = ARTClientOptions(key: "your-api-key") options.clientId = "bob" let realtime = ARTRealtime(options: options) let channel = realtime.channels.get("your-channel-name") channel.presence.subscribe(.enter) { member in print("Member \(member.clientId) entered") } channel.presence.enter(nil) ``` ```realtime_go realtime, err := ably.NewRealtime( ably.WithKey("your-api-key"), ably.WithClientID("bob"), ) if err != nil { log.Fatalf("Failed to create Ably Realtime client: %v", err) } channel := realtime.Channels.Get("your-channel-name") _, err = channel.Presence.Subscribe( context.Background(), ably.PresenceActionEnter, func(msg *ably.PresenceMessage) { log.Printf("Member %s entered\n", msg.ClientID) }) if err != nil { log.Fatalf("Failed to subscribe to presence events: %v", err) } err = channel.Presence.Enter(context.Background(), nil) if err != nil { log.Fatalf("Failed to enter presence: %v", err) } ``` ```realtime_flutter final clientOptions = ably.ClientOptions( key: 'your-api-key', clientId: 'bob' ); final realtime = ably.Realtime(options: clientOptions); final channel = realtime.channels.get('realtime-chat'); channel .presence .subscribe(action: PresenceAction.enter) .listen((message) { print('Member ${message.clientId} entered'); }); await channel.presence.enter(); ``` ## Large-scale presence sets The number of clients that can be simultaneously present on a channel is [limited](https://ably.com/docs/platform/pricing/limits#channel). This ensures the rate of presence messages remains supportable, as it is common for all members on a channel to change state at a similar time. Each presence update (enter, update, leave) is counted as a message for billing purposes, and presence follows an n-squared pattern when you have both subscribers and members present on a channel. This can lead to a surprisingly large number of messages being generated in a short time. As an example, consider 200 clients subscribed to presence events on a channel and all of them join and leave the presence set within a few minutes. This would result in the following messages: * 200 presence messages published for the enter event. * 200 × 200 (40,000) messages subscribed to for the enter events. * 200 presence messages published for the leave event. * 200 × 200 (40,000) presence messages subscribed to for the leave events. 80,400 messages could be sent in a very short space of time on a single channel, which could result in rate limit issues or unexpected message costs. If your application needs all clients subscribed to presence messages then [enabling server-side batching](#server-side-batch) can reduce the number of events received by clients. If your application doesn't need all clients subscribed then you can set some of them to [be present without subscribing](#present-no-subscribe). ### Server-side batching of presence events Enabling [server-side batching](https://ably.com/docs/messages/batch#server-side) for a channel means that Ably will group any messages that are published within a set period of time into batches. These batches are then delivered to subscribers as a single message. The interval over which this batching occurs is configurable. The trade-off between cost efficiency and user experience is appropriate, as a higher interval will increase the latency between message deliveries. This is usually less impactful to user experience for presence events than for regular messages. Using server-side batching for presence events can reduce message costs when the membership of a channel is constantly changing. It also allows for a higher number of presence members per channel to be supported. By default, the number of presence members per channel is [limited](https://ably.com/docs/platform/pricing/limits#channel) to 200. With server-side batching enabled, this increases up to 20,000 clients depending on your [package type](https://ably.com/docs/pricing). ### Be present without subscribing to presence events Messages are streamed to clients as soon as they [attach](https://ably.com/docs/channels/states#attach) to a channel, as long as they have the [subscribe capability](https://ably.com/docs/auth/capabilities). Efficient use of capabilities can prevent clients from receiving presence events while still allowing them to enter the presence set. This is a common scenario when you want lots of people to be present without necessarily listening for presence change events, such as when you just want to know how many people are present on a channel at any point in time. One example of achieving this would be to use one channel for generic communications and another for the presence set. The following capabilities demonstrate this for clients and for servers: For clients: ```json { "chatPresence": ["presence"], "chat": ["publish", "history", "subscribe"], } ``` For servers: ```json { "chatPresence": ["presence", "subscribe"], "chat": ["publish", "history", "subscribe"], } ``` Another approach is to use differential token capabilities on a single channel. The following example shows capabilities where clients can enter presence and publish, but only have subscribe access to regular messages: ```json { "presenceChannel": ["publish", "presence"], "chat": ["presence", "history", "subscribe"] } ``` Alternatively, [channel mode flags](https://ably.com/docs/channels/options#modes) can be used to enable clients to be present on a channel without subscribing to presence events. This also enables clients to still subscribe to regular messages on the channel. ## External presence set maintenance While it's technically possible to use [webhooks](https://ably.com/docs/platform/integrations/webhooks) or other integration rules to maintain an external copy of a channel's presence set, this approach is generally not recommended for several reasons: ### Implementation complexity Combining presence events into a presence set is not trivial to implement. Presence events are fundamentally a stream of delta updates to a CRDT (Conflict-free Replicated Data Type). These updates must be merged into your local copy following a detailed protocol to ensure: * Events can be applied in any order (commutative). * Synthetic leave events are handled correctly. * Members are properly identified by both `clientId` and `connectionId`. * State conflicts are resolved consistently. Ably's realtime client libraries handle this complex logic automatically, but external implementations must replicate this behavior correctly. ### No synchronization mechanism External integration rules receive only a stream of updates with no way to retrieve the complete current state. If your server goes down or fails to process messages, you become permanently out of sync with no built-in way to recover. Key limitations include: * Ably retries failed webhooks within limits, but sufficiently old messages are eventually discarded. * There are caps on messages per webhook post and total messages per second. * Missing events cannot be retrieved after the fact. ### If you must use external maintenance If you decide to proceed with webhook-based presence maintenance despite these limitations, you can use [`PresenceMessage.fromEncodedArray()`](https://ably.com/docs/api/realtime-sdk/presence#presence-from-encoded-array) to decode presence message arrays and translate numerical actions into readable strings. This method also handles data decoding and decryption if you're using [encryption](https://ably.com/docs/channels/options/encryption). For applications requiring an always-up-to-date presence set, use Ably's realtime client libraries and attaching to the channel directly. ## Retrieve presence members The membership of the presence set can be retrieved by calling the [`get()`](https://ably.com/docs/api/realtime-sdk/presence#get) method on the `Presence` object of a channel. This returns an array of all members currently present on the channel and is available using the REST and realtime interfaces of an Ably SDK. An Ably client connected using the realtime interface of an SDK is responsible for keeping track of the presence set from the time that the channel is attached. An up to date presence set is pushed to the client following a channel attachment, and the presence set is updated on each subsequent presence event. [`get()`](https://ably.com/docs/api/realtime-sdk/presence#get) returns the already known presence set retained in memory and does not trigger a new request to the Ably service. The REST interface of an Ably SDK queries [the REST API](https://ably.com/docs/api/rest-api#presence) directly. No presence state is cached in the SDK itself. The following is an example of retrieving the presence set for a channel: ```realtime_javascript const presenceSet = await channel.presence.get(); if (presenceSet.length > 0) { console.log('There are ' + presenceSet.length + ' members on this channel'); console.log('The first member has client ID: ' + presenceSet[0].clientId); } else { console.log('There are no members in this channel'); } ``` ```realtime_nodejs const presenceSet = await channel.presence.get(); if (presenceSet.length > 0) { console.log('There are ' + presenceSet.length + ' members on this channel'); console.log('The first member has client ID: ' + presenceSet[0].clientId); } else { console.log('There are no members in this channel'); } ``` ```realtime_ruby channel.presence.get do |members| puts "There are #{members.size} members on this channel" puts "The first member has client ID: #{members.first.client_id}" end ``` ```realtime_java PresenceMessage[] members = channel.presence.get(); System.out.println("There are " + members.length + " members on this channel"); System.out.println("The first member has client ID: " + members[0].clientId); ``` ```realtime_csharp IEnumerable presence = await channel.Presence.GetAsync(); Console.WriteLine($"There are {presence.Count()} members on this channel"); Console.WriteLine($"The first member has client ID: {presence.First().ClientId}"); ``` ```realtime_objc [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { NSLog(@"There are %lu members on this channel", [members count]); NSLog(@"The first member has client ID: %@", members[0].clientId); }]; ``` ```realtime_swift channel.presence.get { members, error in print("There are \(members.count) members on this channel") print("The first member has client ID: \(members[0].clientId)") } ``` ```realtime_go page, err := channel.Presence.Get(nil) fmt.Println("%d messages in first page\n", len(page.PresenceMessages())) if page.hasNext() { page2, err := page.Next() fmt.Println("%d messages on 2nd page!\n", len(page2.PresenceMessages())) } ``` ```realtime_flutter final presenceSet = await channel.presence.get(); if (presenceSet.isNotEmpty) { print('There are ${presenceSet.length} members in this channel'); print('The first member has client ID: ${presenceSet[0].clientId}'); } else { print('There are no members in this channel'); } ``` ```rest_javascript const presenceSet = await channel.presence.get(); if (presenceSet.length > 0) { console.log('There are ' + presenceSet.length + ' members on this channel'); console.log('The first member has client ID: ' + presenceSet[0].clientId); if (presenceSet.hasNext()) { var currentPresenceSet = await presenceSet.next(); console.log('The first member on the next page has client ID: ' + currentPresenceSet[0].clientId); } } else { console.log('There are no members in this channel'); } ``` ```rest_nodejs const presenceSet = await channel.presence.get(); if (presenceSet.length > 0) { console.log('There are ' + presenceSet.length + ' members on this channel'); console.log('The first member has client ID: ' + presenceSet[0].clientId); if (presenceSet.hasNext()) { var currentPresenceSet = await presenceSet.next(); console.log('The first member on the next page has client ID: ' + currentPresenceSet[0].clientId); } } else { console.log('There are no members in this channel'); } ``` ```rest_ruby members_page = channel.presence.get puts "#{members_page.items.length} presence members in first page" if members_page.has_next? next_page = members_page.next end ``` ```rest_python members_page = channel.presence.get() print str(len(members_page.items)) + ' members present' if members_page.has_next(): next_page = members_page.next() ``` ```rest_php $membersPage = $channel->presence->get(); echo(count($membersPage->items) . ' presence members in first page'); if ($membersPage->hasNext()) { $nextPage = $membersPage.next(); } ``` ```rest_java PaginatedResult membersPage = channel.presence.get(null); System.out.println(membersPage.items().length + " members in first page"); if(membersPage.hasNext()) { PaginatedResult nextPage = membersPage.next(); System.out.println(nextPage.items().length + " members on 2nd page"); } ``` ```rest_csharp PaginatedResult membersPage = await channel.Presence.GetAsync(); Console.WriteLine(membersPage.Items.Count + " members in first page"); if(membersPage.HasNext) { PaginatedResult nextPage = await membersPage.NextAsync(); Console.WriteLine(nextPage.Items.Count + " members on 2nd page"); } ``` ```rest_objc [channel.presence get:^(ARTPaginatedResult *membersPage, ARTErrorInfo *error) { NSLog(@"%lu members in first page", [membersPage.items count]); if (membersPage.hasNext) { [membersPage next:^(ARTPaginatedResult *nextPage, ARTErrorInfo *error) { NSLog(@"%lu members on 2nd page", [nextPage.items count]); }]; } }]; ``` ```rest_swift channel.presence.get { membersPage, error in let membersPage = membersPage! print("\(membersPage.items.count) in first page") if membersPage.hasNext { membersPage.next { nextPage, error in print("\(nextPage!.items.count) members on 2nd page") } } } ``` ```rest_go page, err := channel.Presence.Get(nil) fmt.Println("%d messages in first page\n", len(page.PresenceMessages())) if page.hasNext() { page2, err := page.Next() fmt.Println("%d messages on 2nd page!\n", len(page2.PresenceMessages())) } ``` ```rest_flutter final presenceSet = await channel.presence.get(); if (presenceSet.isNotEmpty) { print('There are ${presenceSet.length} members in this channel'); print('The first member has client ID: ${presenceSet[0].clientId}'); } else { print('There are no members in this channel'); } ``` ### Synced presence set A common requirement of the presence set is to keep an updated list of members that are currently present on a channel in your user interface. Many developers try to build the initial list using the [`get()`](https://ably.com/docs/api/realtime-sdk/presence#get) method and then mutate that list whenever a new presence event arrives. Don't use this approach because it's easy to quickly go wrong and end up with a list that's out of sync with the real presence set. One common error is to fail to take into account that a single `clientId` may be present multiple times on the same channel via different [connections](https://ably.com/docs/connect). These are different members of the presence set as they have different `connectionId`s. For example, if a client with the ID "Sarah" is connected to a channel on both a desktop and a mobile device simultaneously, or via multiple tabs in a browser, "Sarah" will be present twice in the presence set with the same `clientID`. If "Sarah" leaves the channel on her mobile, your app sees the `leave` event and incorrectly removes her entry from the list. Instead just `get()` the presence set afresh from the Ably SDK whenever you see a presence event and use it to rebuild the list of members in your user interface. The `get()` operation is free and a local call to the SDK and provides the presence set that the client has already synced with the server. The server keeps the presence set up to date and there is no cost to using this approach. The following is an example of calling the `get()` method on presence messages: ```realtime_javascript await channel.presence.subscribe(async (presenceMessage) => { // Ignore the presence message, just rebuild your state from the get() method // (including uniquifying by clientId if you only want one entry per clientId) const presenceSet = await channel.presence.get(); if (presenceSet.length > 0) { console.log('There are ' + presenceSet.length + ' members on this channel'); } else { console.log('There are no members on this channel'); } }); ``` ```realtime_go _, err = channel.Presence.SubscribeAll(context.Background(), func(msg *ably.PresenceMessage) { presenceSet, err := channel.Presence.Get(context.Background()) if err != nil { log.Printf("Failed to get presence set: %v", err) return } if len(presenceSet) > 0 { log.Printf("There are %d members on this channel\n", len(presenceSet)) } else { log.Println("There are no members on this channel") } }) if err != nil { log.Fatalf("Failed to subscribe to presence events: %v", err) } ``` ```realtime_java channel.presence.subscribe(new Presence.PresenceListener() { @Override public void onPresenceMessage(PresenceMessage presenceMessage) { // Ignore the presence message, just rebuild your state from the get() method try { PresenceMessage[] presenceSet = channel.presence.get(); if (presenceSet.length > 0) { System.out.println("There are " + presenceSet.length + " members on this channel"); } else { System.out.println("There are no members on this channel"); } } catch (AblyException e) { System.out.println(e.getMessage()); throw new RuntimeException(e); } } }); ``` ### Batch presence It is possible to retrieve the presence state of multiple channels in a single request. The presence state contains details about the clients in the presence set, such as their `clientId`, member data and [presence action](https://ably.com/docs/api/realtime-sdk/types#presence-action). A batch request has a single set of request details containing the request body, parameters and headers. These are converted into an array of requests to the underlying API. Each individual request to the underlying API is performed in parallel and may succeed or fail independently. The following is an example of a batch presence request using the [`request()`](https://ably.com/docs/api/rest-sdk#request) method to query the [batch REST API](https://ably.com/docs/api/rest-api#batch-presence): ```rest_javascript const ablyRest = new Ably.Rest({ key: 'your-api-key' }) const content = { 'channel': 'channel1,channel2' } const presenceSets = await ablyRest.request('GET', '/presence', 3, null, content, null); console.log('Success! status code was ' + presenceSets.statusCode); ``` ```rest_php $rest = new Ably\AblyRest( ['key' => 'your-api-key'] ); $content = ['channel' => 'channel1,channel2']; $presenceSets = $rest->request('GET', '/presence', $content); if (!$presenceSets->success) { echo('An error occurred; err = ' . $presenceSets->errorMessage); } else { echo('Success! status code was ' . strval($presenceSets->statusCode)); } ``` ```rest_java ClientOptions options = new ClientOptions("your-api-key"); AblyRest rest = new AblyRest(options); Param[] params = new Param[] { new Param("channels", "channel1,channel2") }; HttpPaginatedResponse response = rest.request("GET", "/presence", params, null, null); System.out.println("Success! Status code was " + response.statusCode); ``` #### Batch presence requests A batch presence request contains a comma separated list of channel names with no spaces. The following is an example of a batch presence request using the [`request()`](https://ably.com/docs/api/rest-sdk#request) method to query the [batch REST API](https://ably.com/docs/api/rest-api#batch-presence): ```rest_javascript var ablyRest = new Ably.Rest({ key: 'your-api-key' }) const content = { 'channel': 'quickstart,channel2' } const presenceSets = await rest.request('GET', '/presence', 3, content, null, null); console.log('Success! status code was ' + presenceSets.statusCode); ``` ```rest_php $rest = new Ably\AblyRest( ['key' => 'your-api-key'] ); $content = ['channel' => 'quickstart,channel2']; $presenceSets = $rest->request('GET', '/presence', $content); if (!$presenceSets->success) { echo('An error occurred; err = ' . $presenceSets->errorMessage); } else { echo('Success! status code was ' . strval($presenceSets->statusCode)); } ``` ```rest_java ClientOptions options = new ClientOptions("your-api-key"); AblyRest rest = new AblyRest(options); Param[] params = new Param[] { new Param("channels", "channel1,channel2") }; HttpPaginatedResponse response = rest.request("GET", "/presence", params, null, null); System.out.println("Success! Status code was " + response.statusCode); ``` The following is an example curl request, querying the REST API directly: ```shell curl -X GET https://main.realtime.ably.net/presence?channel=quickstart,channel2 \ -u "your-api-key" ``` #### Batch presence responses Once all requests have been completed in a batch request, a batch response is returned with three possible outcomes: - **Success** - If all of the individual requests were successful then an array containing the response of each query is returned in request order. - **Failure** - If the batch request itself failed before the individual requests were made, then an error response is returned with a status code and error response body. Examples of why the batch request can fail include an authorization failure or an invalid request. - **Partial success** - If one or more of the individual requests failed the response body contains an error object with the error code `40020` and a status code of `400`. The error body contains a `batchResponse` array of each individual response in request order. The `batchResponse` can be inspected if there is a need to know the details of each outcome. If you only need to know whether or not the batch request was completely successful then the status code is sufficient. The examples for each possible outcome will use the following request data: ```text { channel: 'channel0,channel1,channel2' } ``` The following is an example of a successful batch presence response. The response body contains details of each client present on the channel and the channel they are present on. The status code for a successful response is always `200`: ```json [ { "channel":"channel0", "presence":[ {"clientId": "user1", "action": "1"}, {"clientId": "user2", "action": "1"} ] }, { "channel":"channel1", "presence":[] }, { "channel":"channel2", "presence":[ {"clientId": "user2", "action": "1"}, {"clientId": "user3", "action": "1"} ] } ] ``` The following is an example of a batch presence failure response. The response body contains the details of the `error`, in this example that the token used for the request has expired. The status code is `401`: ```json { "error": { "message":"Token expired", "statusCode":401, "code":40140 } } ``` The following is an example of a batch presence partial success response. The successful requests contain the details of each client present on the channel, and the channel they are present on. The failed request contains the channel the request failed for and the details of the `error`, in this example that the credentials used didn't have the capability to query that channel. The status code for a partial success is always `400`: ```json { "error": { "message": "Batched response includes errors", "statusCode":400, "code":40020 } "batchResponse": [ { "channel":"channel0", "presence":[ {"clientId": "user1", "action": "1"}, {"clientId": "user2", "action": "1"} ] }, { "channel":"channel1", "presence":[] }, { "channel":"channel2", "error": { "message": "Given credentials do not have the required capability", "statusCode": 401, "code": 40160 } } ] } ``` #### Handling batch presence responses Using the request() method with the REST interface of an Ably SDK requires handling responses for success, failure and partial success. A `response` object will have `response.success` set to true if the batch presence request was successful, and false if at least one individual presence request failed. The `response.statusCode` will be set to 200 for success, 400 for partial success and 401 for an expected failure. `response.errorCode` will then contain the Ably error code and `response.errorMessage` will contain the details of the error. `response.items` will contain a list of responses for each channel for which presence has been requested in a successful response. `response.items.batchResponse` will contain a list of each channel's results, be it an error or a success for a partially successful response. The `response.errorCode` will always be `40020` for a partial success. The following is an example of handling the various responses: ```rest_javascript var ablyRest = new Ably.Rest({ key: 'your-api-key' }) const content = { 'channel': 'channel1,channel2' } const presenceSets = await ablyRest.request('GET', '/presence', 3, null, content, null); console.log(presenceSets.success); console.log(presenceSets.errorCode); if (presenceSets.success) { // If complete success for (i = 0; i < presenceSets.items.length; i++) { // Each presenceSets item will be roughly of the style: /* { 'channel': 'channel1', 'presence': [ { 'action': 1, 'clientId': 'CLIENT1' }, { 'action': 1, 'clientId': 'CLIENT2' } ] } */ } } else if (presenceSets.errorCode === 40020) { // If partial success for (i = 0; i < presenceSets.items[0].batchResponse.length; i++) { // Each batchResponse item will either be the same as success if it succeeded, or: /* { 'channel': 'channel1', 'error': { 'code': 40160, 'message': 'ERROR_MESSAGE', 'statusCode': 401 } } */ } } else { // If failed, check why console.log(presenceSets.errorCode + ', ' + presenceSets.errorMessage); } ``` ```rest_php $rest = new Ably\AblyRest( ['key' => 'your-api-key'] ); $content = ['channel' => 'channel1,channel2']; $presenceSets = $rest->request('GET', '/presence', $content); var_dump('Is a success? ' . $presenceSets->success); var_dump('Has an error code? ' . $presenceSets->errorCode); if ($presenceSets->success) { // If complete success for ($i = 0; $i < count($presenceSets->items); $i++) { // Each presenceSets item will be roughly of the style: /* { 'channel': 'channel1', 'presence': [ { 'action': 1, 'clientId': 'CLIENT1' }, { 'action': 1, 'clientId': 'CLIENT2' } ] } */ } } elseif ($presenceSets->errorCode === 40020) { // If partial success for ($i = 0; $i < count($presenceSets->items[0]->batchResponse); $i++) { // Each batchResponse item will either be the same as success if it succeeded, or: /* { 'channel': 'channel1', 'error': { 'code': 40160, 'message': 'ERROR_MESSAGE', 'statusCode': 401 } } */ } } else { // If failed, check why var_dump($presenceSets->errorCode . ', ' . $presenceSets->errorMessage); } ``` ## Presence history [History](https://ably.com/docs/storage-history/history) provides access to instantaneous "live" history as well as the longer term persisted history for presence channels. If [persisted history](https://ably.com/docs/storage-history/history#presence-history) is enabled for the channel, then presence events will be stored for 24 or 72 hours, depending on your account package. If persisted history is not enabled, Ably retains the last two minutes of presence event history in memory. ## Handle unstable connections and failures Any time a channel is re-attached and the presence set is re-synced, e.g. after a short disconnection, the client will check whether any members it has entered into the presence set are there. If not, it will automatically re-enter them. This means that if a channel loses continuity (for example, because a client was disconnected from Ably for more than two minutes before reconnecting), then after the channel automatically re-attaches, any presence members it had previously entered will be restored. The exception is if you use the [recover](https://ably.com/docs/connect/states#recover) feature to resume a previous connection with a fresh SDK instance (for example, to have continuity over a page refresh). In that case you will need to explicitly re-enter presence after you re-attach to the channel, due to the loss of SDK internal state. Clients that are part of a presence set remain present for 15 seconds after they are abruptly disconnected, for example where the internet connection suddenly drops or the client is changing networks. This delay is to avoid repeated `leave` and `enter` events being sent when a client is experiencing an unstable connection. The Ably SDK will attempt to [reconnect](https://ably.com/docs/connect/states) after a disconnect. If the connection is reestablished before 15 seconds have passed, a `leave` event will not be sent. If the connection is reestablished after 15 seconds, a `leave` event will be sent and the presence set will need to be rejoined. Note that the 15 second delay from being removed from the presence set is only for abrupt or unplanned disconnects. If a client calls [`leave()`](https://ably.com/docs/api/realtime-sdk/presence#leave) or [`close()`](https://ably.com/docs/api/realtime-sdk/connection#close) they immediately send a `leave` event. The time taken before a `leave` event is sent in the case of an abrupt disconnect can be reduced to a minimum of 1 second by setting a value for `remainPresentFor`, in milliseconds. This property is set within the `transportParams` property of the [`clientOptions`](https://ably.com/docs/api/realtime-sdk#client-options) object. It can initially take up to 30 seconds to identify that a client has been abruptly disconnected. Shorten the amount of time taken to identify abrupt disconnects using the [`heartbeatInterval`](https://ably.com/docs/connect#heartbeats) property if your app needs to quickly identify presence set members being abruptly disconnected. The following example code demonstrates establishing a connection to Ably with `remainPresentFor` set to 1 second: ```javascript const ably = new Ably.Realtime( { key: 'your-api-key', transportParams: { remainPresentFor: 1000 } } ); ``` ```flutter final clientOptions = ably.ClientOptions( key: 'your-api-key', transportParams: {'remainPresentFor': '1000'}, ); final realtime = ably.Realtime(options: clientOptions); ``` ```java ClientOptions options = new ClientOptions("your-api-key"); options.transportParams = new Param[] { new Param("remainPresentFor", "1000") }; AblyRealtime ablyRealtime = new AblyRealtime(options); ``` ### Browser tab close behavior You may notice that closing a browser tab results in a 15-second delay before presence members leave the presence set. By default, the client library closes the connection when the page is closed or refreshed, which should cause presence members to leave immediately. However, this may not occur in certain scenarios: * If you have explicitly set the `closeOnUnload` client option to `false` (for example, when using connection state recovery), the connection won't close immediately on page unload. This option defaults to `true` in current versions. * When using the `recover` client option and closing a tab (rather than refreshing), presence members remain for 15 seconds. The recover option stores the recovery key in the browser's localStorage to resume connections after page refresh, but cannot distinguish between tab close and refresh events. * Some browsers don't allow sufficient time for the `beforeunload` handler to notify the server when tabs are closed or refreshed. This affects some versions of Safari, Internet Explorer, and other browsers. * When Chrome's Memory Saver feature discards tabs, it doesn't fire a `beforeunload` event, resulting in the 15-second delay before presence members are removed.