Skip to main content

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

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();

Listen for Changes

// 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 Events

EventDescriptionData
presence_syncFull snapshot of all tracked userspresences[] — array of { userId, state }
presence_joinA user started trackinguserId, connectionId, state
presence_leaveA user stopped tracking or disconnecteduserId, connectionId
  • onSync fires every time the presence list changes — it gives you the complete, current list of all tracked users.
  • onJoin and onLeave fire for individual changes.
  • Use onSync for rendering the full online list; use onJoin/onLeave for 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"
}
FieldTypeDescription
userIdstringAuthenticated user ID
connectionIdstringUnique WebSocket connection identifier
stateobjectUser-defined presence state (max 1 KB)
reasonstringLeave 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:

  1. Removes the user from the presence map
  2. Broadcasts presence_leave to 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)
},
});
SettingDefaultDescription
presenceTTL60000 (60s)Time in ms before an idle presence entry is removed. Should be ≥ 2× the client ping interval (30s).
tip

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 },
});
State Design Tips

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:

  1. On wake-up, the server sends PRESENCE_RESYNC to all connected sockets (both authenticated and unauthenticated)
  2. Each SDK that has active tracking automatically re-sends its current presence state via a track message
  3. The presence map is rebuilt from the re-tracked states
  4. onSync fires 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.

RESYNC Asymmetry

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.