Presence
Track online users and their status in real time. Presence is ideal for showing who's online, cursor positions in collaborative editors, typing indicators, and player states in games.
Track Your State
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
const presence = client.realtime.presence('game-room');
// Track your state
presence.track({
username: 'Jane',
status: 'online',
cursor: { x: 100, y: 200 },
});
// Update state (replaces previous state)
presence.track({
username: 'Jane',
status: 'away',
});
// Remove tracking
presence.untrack();
final presence = client.realtime.presence('game-room');
presence.track({
'username': 'Jane',
'status': 'online',
});
presence.untrack();
let presence = client.realtime.presence("game-room")
presence.track([
"username": "Jane",
"status": "online"
])
presence.untrack()
val presence = client.realtime.presence("game-room")
presence.track(mapOf("username" to "Jane", "status" to "online"))
presence.untrack()
var presence = client.realtime().presence("game-room");
presence.track(Map.of("username", "Jane", "status", "online"));
presence.untrack();
var presence = client.Realtime.Presence("game-room");
presence.Track(new() {
["username"] = "Jane",
["status"] = "online",
});
presence.Untrack();
auto presence = client.realtime().presence("game-room");
presence.track(R"({"username": "Jane", "status": "online"})");
presence.untrack();
Listen for Changes
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
// Full state of all online users (fires on every change)
presence.onSync((users) => {
console.log('Online users:', users);
});
// New user joined
presence.onJoin((userId, connectionId, state) => {
console.log('Joined:', userId, state);
});
// User left
presence.onLeave((userId, connectionId) => {
console.log('Left:', userId);
});
presence.onSync((users) {
print('Online users: $users');
});
presence.onJoin((userId, connectionId, state) {
print('Joined: $userId $state');
});
presence.onLeave((userId, connectionId) {
print('Left: $userId');
});
presence.onSync { users in
print("Online users: \(users)")
}
presence.onJoin { userId, connectionId, state in
print("Joined: \(userId) \(state)")
}
presence.onLeave { userId, connectionId in
print("Left: \(userId)")
}
presence.onSync { users ->
println("Online users: $users")
}
presence.onJoin { userId, connectionId, state ->
println("Joined: $userId $state")
}
presence.onLeave { userId, connectionId ->
println("Left: $userId")
}
presence.onSync(users -> {
System.out.println("Online: " + users);
});
presence.onJoin((userId, connectionId, state) -> {
System.out.println("Joined: " + userId);
});
presence.onLeave((userId, connectionId) -> {
System.out.println("Left: " + userId);
});
presence.OnSync(users => {
Console.WriteLine($"Online: {users.Count} users");
});
presence.OnJoin((userId, connectionId, state) => {
Console.WriteLine($"Joined: {userId}");
});
presence.OnLeave((userId, connectionId) => {
Console.WriteLine($"Left: {userId}");
});
presence.onSync([](const std::vector<eb::PresenceState>& users) {
std::cout << "Online: " << users.size() << " users" << std::endl;
});
presence.onJoin([](const std::string& userId, const std::string& connId, const nlohmann::json& state) {
std::cout << "Joined: " << userId << std::endl;
});
presence.onLeave([](const std::string& userId, const std::string& connId) {
std::cout << "Left: " << userId << std::endl;
});
Presence Events
| Event | Description | Data |
|---|---|---|
presence_sync | Full snapshot of all tracked users | presences[] — array of { userId, state } |
presence_join | A user started tracking | userId, connectionId, state |
presence_leave | A user stopped tracking or disconnected | userId, connectionId |
onSyncfires every time the presence list changes — it gives you the complete, current list of all tracked users.onJoinandonLeavefire for individual changes.- Use
onSyncfor rendering the full online list; useonJoin/onLeavefor animations or notifications.
WebSocket Message Formats
Client sends presence_join:
{
"type": "presence_join",
"userId": "user-123",
"connectionId": "conn-abc",
"state": { "status": "online", "cursor": { "x": 0, "y": 0 } }
}
Server broadcasts presence_update:
{
"type": "presence_update",
"userId": "user-123",
"connectionId": "conn-abc",
"state": { "status": "away" }
}
Server broadcasts presence_leave:
{
"type": "presence_leave",
"userId": "user-123",
"connectionId": "conn-abc",
"reason": "timeout"
}
| Field | Type | Description |
|---|---|---|
userId | string | Authenticated user ID |
connectionId | string | Unique WebSocket connection identifier |
state | object | User-defined presence state (max 1 KB) |
reason | string | Leave reason — "disconnect" (clean close) or "timeout" (TTL expired) |
Auto-Cleanup on Disconnect
When a WebSocket connection closes (network drop, tab close, explicit disconnect), the server automatically:
- Removes the user from the presence map
- Broadcasts
presence_leaveto all remaining clients
No manual cleanup is needed — disconnected users are removed instantly.
Presence TTL
For clients that become unreachable without a clean disconnect (network hang, app force-kill), the server automatically removes stale presence entries based on a configurable TTL.
The SDK sends ping messages every 30 seconds. The server tracks the last ping timestamp for each presence entry. If no ping is received within the TTL window, the entry is removed and presence_leave is broadcast with reason: 'timeout'.
// edgebase.config.ts
export default defineConfig({
realtime: {
presenceTTL: 60000, // 60 seconds (default)
},
});
| Setting | Default | Description |
|---|---|---|
presenceTTL | 60000 (60s) | Time in ms before an idle presence entry is removed. Should be ≥ 2× the client ping interval (30s). |
The default TTL of 60 seconds (2× the 30-second ping interval) provides a good balance — it allows for one missed ping before declaring the client unreachable.
State Size Limit
Presence state objects are limited to 1 KB per user per channel. Both the server and SDK validate the size before sending.
// This will throw an error if state exceeds 1KB
presence.track({
username: 'Jane',
status: 'online',
cursor: { x: 100, y: 200 },
});
Keep your presence state small and focused. Include only what other users need to see.
Good — minimal, relevant data:
presence.track({ name: 'Jane', status: 'typing', cursor: { x: 120, y: 340 } });
Bad — too much data, risks hitting the 1KB limit:
presence.track({ fullProfile: {...}, allSettings: {...}, recentHistory: [...] });
Channel Access
Presence channels use realtime.namespaces rules. A common pattern is to require authentication:
realtime: {
namespaces: {
'presence:*': {
access: {
subscribe(auth) { return auth !== null },
},
},
},
}
See Access Rules for wildcard patterns, default policies, and more.
Hibernation Recovery
When a Durable Object hibernates (no active messages) and wakes up, the in-memory presence map is lost. The server handles recovery automatically:
- On wake-up, the server sends
PRESENCE_RESYNCto all connected sockets (both authenticated and unauthenticated) - Each SDK that has active tracking automatically re-sends its current presence state via a
trackmessage - The presence map is rebuilt from the re-tracked states
onSyncfires with the reconstructed presence list
This happens transparently — you don't need to handle it in your application code. The SDK's enableResync() method (called internally) ensures your tracked state is automatically re-sent.
PRESENCE_RESYNC is sent to all sockets (including unauthenticated), while FILTER_RESYNC is sent only to authenticated sockets. This gives every client a chance to re-authenticate and re-track their presence after hibernation.