Loading content...
When you open Slack and see a green dot next to your colleague's name, or notice "Active now" under a friend's profile on Instagram, you're observing a presence system in action. This seemingly simple feature—knowing who's online right now—is deceptively complex at scale.
Consider what a presence system must do:
This page dissects presence system architecture at production scale, covering the data structures, consistency tradeoffs, fan-out strategies, and optimization techniques used by industry leaders like Slack, Discord, WhatsApp, and Facebook.
By the end of this page, you will understand: • How presence differs from session management • Presence state machines and status hierarchies • Distributed presence storage strategies • Fan-out patterns for presence updates • How to handle presence in multi-device scenarios • Optimizations for reducing presence traffic at scale
Presence is the real-time availability status of a user in a system. Unlike authentication (who you are) or session management (are you logged in), presence answers: "Is this user actively available right now?"
Presence vs. Related Concepts:
| Concept | Question Answered | Lifespan | Update Frequency |
|---|---|---|---|
| Authentication | Who is this user? | Session lifetime (hours/days) | Once per session |
| Session Management | Is this user logged in? | Until explicit logout/timeout | Rarely changes |
| Presence | Is this user actively available? | Connection-bound (seconds/minutes) | Continuously (connect, idle, disconnect) |
| Last Seen | When was this user last active? | Persistent (forever) | On disconnect or activity |
Presence State Machine:
Most presence systems model user status as a finite state machine with transitions triggered by connection events and activity patterns:
Common Presence States:
| State | Display | Meaning | Trigger |
|---|---|---|---|
| Online | 🟢 Green | User is actively connected and responsive | WebSocket connected + recent activity |
| Idle/Away | 🟡 Yellow | User is connected but inactive | No activity for configurable period (e.g., 5 minutes) |
| Do Not Disturb | 🔴 Red | User is connected but doesn't want notifications | Explicitly set by user |
| Offline | ⚫ Gray | User is not connected | No active WebSocket connection |
| Invisible | ⚫ Gray | User appears offline but is actually connected | User preference to hide presence |
The Invisible State Paradox:
Invisible mode creates interesting challenges. The user must be treated as offline for presence queries, but the system must still deliver messages to their connection. This requires separating the concepts of presence visibility (what others see) from connection status (actual state in the system).
Some systems extend presence beyond online/offline to include rich status: "In a meeting", "Commuting", "On vacation". Slack calls these "status"; Discord has "Custom Status" and "Activity" (playing a game, listening to Spotify). These are layered on top of the core presence state machine, not replacing it.
Designing the presence data model requires balancing storage efficiency, query performance, and update frequency. Here's a production-ready data model:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
package presence import ( "time") // PresenceState represents the core presence statustype PresenceState int const ( StateOffline PresenceState = iota StateOnline StateIdle StateAway StateDND StateInvisible) // UserPresence represents the complete presence information for a usertype UserPresence struct { // Core identification UserID uint64 `json:"user_id"` // Current presence state State PresenceState `json:"state"` // Visibility (what others see - may differ from actual state for invisible users) VisibleState PresenceState `json:"visible_state"` // Timestamps LastStateChange time.Time `json:"last_state_change"` LastActivity time.Time `json:"last_activity"` LastSeen time.Time `json"last_seen"` // Set on disconnect // Rich status (optional) StatusText string `json:"status_text,omitempty"` // "In a meeting" StatusEmoji string `json:"status_emoji,omitempty"` // "📅" StatusExpiry time.Time `json:"status_expiry,omitempty"` // Auto-clear time // Activity (what user is doing) ActivityType string `json:"activity_type,omitempty"` // "playing", "listening" ActivityName string `json:"activity_name,omitempty"` // "Elden Ring" ActivityStart time.Time `json:"activity_start,omitempty"` // Multi-device tracking Connections []ConnectionInfo `json:"connections,omitempty"`} // ConnectionInfo tracks individual device connectionstype ConnectionInfo struct { ConnectionID string `json:"connection_id"` DeviceType string `json:"device_type"` // "desktop", "mobile", "web" Platform string `json:"platform"` // "Windows", "iOS", "Android", "macOS" ConnectedAt time.Time `json:"connected_at"` LastActivity time.Time `json:"last_activity"` State PresenceState `json:"state"`} // PresenceUpdate represents a change that needs to be broadcasttype PresenceUpdate struct { UserID uint64 `json:"user_id"` OldState PresenceState `json:"old_state"` NewState PresenceState `json:"new_state"` VisibleState PresenceState `json:"visible_state"` Timestamp time.Time `json:"timestamp"` // Optional rich status changes StatusChanged bool `json:"status_changed,omitempty"` StatusText string `json:"status_text,omitempty"` StatusEmoji string `json:"status_emoji,omitempty"` // Optional activity changes ActivityChanged bool `json:"activity_changed,omitempty"` ActivityType string `json:"activity_type,omitempty"` ActivityName string `json:"activity_name,omitempty"`} // AggregatedPresence represents multi-device presence// The user's displayed state is the "most present" across all devicestype AggregatedPresence struct { UserID uint64 State PresenceState // Aggregated state DeviceCount int // Number of active connections PrimaryDevice string // Device showing most activity} // GetAggregatedState computes overall presence from multiple connectionsfunc (p *UserPresence) GetAggregatedState() PresenceState { if len(p.Connections) == 0 { return StateOffline } // Priority: DND > Online > Idle > Away > Invisible // Return the "most present" state across all devices bestState := StateOffline for _, conn := range p.Connections { if presencePriority(conn.State) > presencePriority(bestState) { bestState = conn.State } } // Apply invisible override (user wants to appear offline) if p.State == StateInvisible { return StateOffline } return bestState} func presencePriority(state PresenceState) int { switch state { case StateDND: return 4 case StateOnline: return 3 case StateIdle: return 2 case StateAway: return 1 default: return 0 }}Storage Schema for Presence:
Presence data has unique access patterns requiring specialized storage strategies:
| Data Type | Storage | Format | Rationale |
|---|---|---|---|
| Current presence | Redis Hash | user:{userID}:presence → hash fields | Fast reads, atomic updates, TTL for cleanup |
| Connection tracking | Redis Set/Hash | user:{userID}:connections → set of connection IDs | Track multi-device presence |
| Last seen (persistent) | PostgreSQL/Cassandra | users table or separate last_seen table | Historical data, survives Redis restart |
| Presence by channel/room | Redis Set | channel:{channelID}:presence → set of user IDs | Quickly query all online users in a context |
| Subscriber lists | Redis Set | user:{userID}:subscribers → set of user IDs | Who cares about this user's presence |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
package presence import ( "context" "encoding/json" "fmt" "strconv" "time" "github.com/go-redis/redis/v8") // PresenceStore handles presence data storage and retrievaltype PresenceStore struct { redis *redis.Client ttl time.Duration // TTL for presence entries (e.g., 5 minutes, renewed on activity)} func NewPresenceStore(redis *redis.Client) *PresenceStore { return &PresenceStore{ redis: redis, ttl: time.Minute * 5, }} // Keysfunc presenceKey(userID uint64) string { return fmt.Sprintf("presence:%d", userID)} func connectionKey(userID uint64) string { return fmt.Sprintf("presence:%d:connections", userID)} func channelPresenceKey(channelID uint64) string { return fmt.Sprintf("channel:%d:presence", channelID)} // SetPresence updates user presence atomicallyfunc (ps *PresenceStore) SetPresence(ctx context.Context, presence *UserPresence) error { key := presenceKey(presence.UserID) // Use Redis Hash for structured presence data fields := map[string]interface{}{ "state": int(presence.State), "visible_state": int(presence.VisibleState), "last_state_change": presence.LastStateChange.Unix(), "last_activity": presence.LastActivity.Unix(), "status_text": presence.StatusText, "status_emoji": presence.StatusEmoji, "activity_type": presence.ActivityType, "activity_name": presence.ActivityName, } pipe := ps.redis.Pipeline() // Set presence hash pipe.HSet(ctx, key, fields) // Set TTL (presence auto-expires if not renewed) pipe.Expire(ctx, key, ps.ttl) _, err := pipe.Exec(ctx) return err} // GetPresence retrieves single user's presencefunc (ps *PresenceStore) GetPresence(ctx context.Context, userID uint64) (*UserPresence, error) { key := presenceKey(userID) result, err := ps.redis.HGetAll(ctx, key).Result() if err != nil { return nil, err } if len(result) == 0 { // No presence data = offline return &UserPresence{ UserID: userID, State: StateOffline, VisibleState: StateOffline, }, nil } return parsePresenceFromHash(userID, result), nil} // GetPresenceBatch retrieves presence for multiple users efficientlyfunc (ps *PresenceStore) GetPresenceBatch(ctx context.Context, userIDs []uint64) (map[uint64]*UserPresence, error) { if len(userIDs) == 0 { return nil, nil } // Use pipeline for batch reads pipe := ps.redis.Pipeline() cmds := make(map[uint64]*redis.StringStringMapCmd, len(userIDs)) for _, userID := range userIDs { cmds[userID] = pipe.HGetAll(ctx, presenceKey(userID)) } _, err := pipe.Exec(ctx) if err != nil && err != redis.Nil { return nil, err } result := make(map[uint64]*UserPresence, len(userIDs)) for userID, cmd := range cmds { data, err := cmd.Result() if err != nil || len(data) == 0 { result[userID] = &UserPresence{ UserID: userID, State: StateOffline, VisibleState: StateOffline, } continue } result[userID] = parsePresenceFromHash(userID, data) } return result, nil} // RenewPresence extends TTL without changing state (called on activity)func (ps *PresenceStore) RenewPresence(ctx context.Context, userID uint64) error { return ps.redis.Expire(ctx, presenceKey(userID), ps.ttl).Err()} // TrackConnection adds a connection to multi-device trackingfunc (ps *PresenceStore) TrackConnection(ctx context.Context, userID uint64, conn ConnectionInfo) error { key := connectionKey(userID) data, err := json.Marshal(conn) if err != nil { return err } pipe := ps.redis.Pipeline() pipe.HSet(ctx, key, conn.ConnectionID, data) pipe.Expire(ctx, key, ps.ttl) _, err = pipe.Exec(ctx) return err} // RemoveConnection removes a connection from trackingfunc (ps *PresenceStore) RemoveConnection(ctx context.Context, userID uint64, connectionID string) error { key := connectionKey(userID) remaining, err := ps.redis.HDel(ctx, key, connectionID).Result() if err != nil { return err } // If no connections remain, user is offline if remaining == 0 { // The presence key will also expire via TTL // But we can proactively delete for immediate effect ps.redis.Del(ctx, presenceKey(userID)) } return nil} // GetChannelPresence returns all online users in a channelfunc (ps *PresenceStore) GetChannelPresence(ctx context.Context, channelID uint64) ([]uint64, error) { key := channelPresenceKey(channelID) result, err := ps.redis.SMembers(ctx, key).Result() if err != nil { return nil, err } userIDs := make([]uint64, 0, len(result)) for _, s := range result { if id, err := strconv.ParseUint(s, 10, 64); err == nil { userIDs = append(userIDs, id) } } return userIDs, nil} // JoinChannel adds user to channel presence setfunc (ps *PresenceStore) JoinChannel(ctx context.Context, userID, channelID uint64) error { return ps.redis.SAdd(ctx, channelPresenceKey(channelID), userID).Err()} // LeaveChannel removes user from channel presence setfunc (ps *PresenceStore) LeaveChannel(ctx context.Context, userID, channelID uint64) error { return ps.redis.SRem(ctx, channelPresenceKey(channelID), userID).Err()} func parsePresenceFromHash(userID uint64, data map[string]string) *UserPresence { p := &UserPresence{UserID: userID} if v, ok := data["state"]; ok { state, _ := strconv.Atoi(v) p.State = PresenceState(state) } if v, ok := data["visible_state"]; ok { state, _ := strconv.Atoi(v) p.VisibleState = PresenceState(state) } if v, ok := data["last_state_change"]; ok { ts, _ := strconv.ParseInt(v, 10, 64) p.LastStateChange = time.Unix(ts, 0) } if v, ok := data["last_activity"]; ok { ts, _ := strconv.ParseInt(v, 10, 64) p.LastActivity = time.Unix(ts, 0) } if v, ok := data["status_text"]; ok { p.StatusText = v } if v, ok := data["status_emoji"]; ok { p.StatusEmoji = v } if v, ok := data["activity_type"]; ok { p.ActivityType = v } if v, ok := data["activity_name"]; ok { p.ActivityName = v } return p}Using Redis TTL for presence entries provides automatic offline detection without explicit disconnect handling. If a connection server crashes, presence entries expire naturally after the TTL period. Active connections renew the TTL on each heartbeat, keeping presence alive.
When a user's presence changes, interested parties must be notified. This is the fan-out problem: one user's status change may need to reach thousands of observers.
Fan-Out Strategies:
| Strategy | Description | Pros | Cons | Best For |
|---|---|---|---|---|
| Push on Change | Immediately broadcast to all interested users | Instant updates, minimal polling | High write amplification for popular users | Small to medium user base |
| Pull on Demand | Clients poll for presence when needed | Low write cost, simple server logic | Stale data, high read load | Low-frequency presence needs |
| Hybrid (Push critical, Pull bulk) | Push to actively viewing users, pull for others | Balanced load and freshness | Complexity in determining who's actively viewing | Large-scale systems |
| Pub/Sub Channels | Users subscribe to channels; publish to channel | Decoupled, scalable | Subscription management overhead | Chat/collaboration apps |
The Popular User Problem:
Consider a celebrity with 10 million followers. When they come online, naively pushing to all followers would generate 10 million messages—potentially overwhelming the system.
Solutions:
Tiered fan-out: Push to users currently viewing the profile; let others poll or receive updates on next load
Rate limiting: Batch presence updates and send at most every N seconds
Interest-based filtering: Only push if the observer has notification enabled for this user
Lazy presence: Popular users' presence is only fetched when explicitly requested, never pushed
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
package presence import ( "context" "encoding/json" "sync" "time" "github.com/go-redis/redis/v8") // PresenceFanout handles distributing presence updates to interested userstype PresenceFanout struct { redis *redis.Client subscriptions *SubscriptionManager batcher *UpdateBatcher} // SubscriptionManager tracks who wants presence updates for whom// Optimized for the common case: users in the same channels/teams care about each othertype SubscriptionManager struct { // Channel-based subscriptions: all members of a channel get each other's presence channelMembers map[uint64][]uint64 // channelID -> []userID // Direct subscriptions: explicit friend/follow relationships directSubs map[uint64][]uint64 // userID -> []subscriberIDs mu sync.RWMutex} // UpdateBatcher reduces fan-out storm by batching rapid presence changestype UpdateBatcher struct { pending map[uint64]*PresenceUpdate // userID -> most recent update mu sync.Mutex flushEvery time.Duration} func NewPresenceFanout(redis *redis.Client) *PresenceFanout { pf := &PresenceFanout{ redis: redis, subscriptions: &SubscriptionManager{ channelMembers: make(map[uint64][]uint64), directSubs: make(map[uint64][]uint64), }, batcher: &UpdateBatcher{ pending: make(map[uint64]*PresenceUpdate), flushEvery: 500 * time.Millisecond, }, } go pf.batcher.run(pf.flushUpdates) return pf} // PublishUpdate queues a presence update for fan-outfunc (pf *PresenceFanout) PublishUpdate(ctx context.Context, update *PresenceUpdate) { // Add to batcher - multiple rapid updates become single notification pf.batcher.add(update)} // flushUpdates sends batched updates to interested usersfunc (pf *PresenceFanout) flushUpdates(updates []*PresenceUpdate) { ctx := context.Background() for _, update := range updates { // Find all users interested in this user's presence subscribers := pf.subscriptions.GetSubscribers(update.UserID) if len(subscribers) == 0 { continue } // Group subscribers by their connection server for efficient delivery serverSubscribers := pf.groupByServer(subscribers) // Publish to each server's presence channel payload, _ := json.Marshal(update) for serverID, userIDs := range serverSubscribers { channel := fmt.Sprintf("presence:server:%s", serverID) message := PresenceDelivery{ Update: update, Recipients: userIDs, } msgPayload, _ := json.Marshal(message) pf.redis.Publish(ctx, channel, msgPayload) } }} type PresenceDelivery struct { Update *PresenceUpdate `json:"update"` Recipients []uint64 `json:"recipients"`} // GetSubscribers returns all users interested in a user's presencefunc (sm *SubscriptionManager) GetSubscribers(userID uint64) []uint64 { sm.mu.RLock() defer sm.mu.RUnlock() subscriberSet := make(map[uint64]struct{}) // Add direct subscribers for _, subID := range sm.directSubs[userID] { subscriberSet[subID] = struct{}{} } // Add channel co-members (they implicitly subscribe to each other) // This requires reverse index: which channels is this user in? // In practice, this would be looked up from storage result := make([]uint64, 0, len(subscriberSet)) for subID := range subscriberSet { if subID != userID { // Don't notify user about their own presence result = append(result, subID) } } return result} // add queues an update, replacing any pending update for the same userfunc (b *UpdateBatcher) add(update *PresenceUpdate) { b.mu.Lock() b.pending[update.UserID] = update b.mu.Unlock()} // run flushes batched updates periodicallyfunc (b *UpdateBatcher) run(flush func([]*PresenceUpdate)) { ticker := time.NewTicker(b.flushEvery) defer ticker.Stop() for range ticker.C { b.mu.Lock() if len(b.pending) == 0 { b.mu.Unlock() continue } updates := make([]*PresenceUpdate, 0, len(b.pending)) for _, update := range b.pending { updates = append(updates, update) } b.pending = make(map[uint64]*PresenceUpdate) b.mu.Unlock() flush(updates) }} func (pf *PresenceFanout) groupByServer(userIDs []uint64) map[string][]uint64 { // In practice, look up which server each user is connected to // This enables targeted delivery to the right connection server return nil // Placeholder}Batching presence updates reduces load but adds latency. A 500ms batch window means users might see presence changes up to 500ms late. For most applications this is acceptable, but real-time games or collaborative editors may need tighter windows or direct push for critical presence events.
Modern users connect from multiple devices simultaneously: desktop at work, phone in pocket, tablet at home, watch on wrist. A presence system must aggregate state across devices sensibly.
Multi-Device Challenges:
State Aggregation: If phone is idle but desktop is active, what's the user's presence?
Disconnect Handling: User closes laptop; phone still connected. Are they offline?
Status Conflicts: User sets DND on phone but not desktop. Which applies?
Activity Detection: Activity on one device; should it reset idle timers on all devices?
Aggregation Strategies:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
package presence import ( "sort" "time") // DevicePresenceAggregator computes overall presence from multiple devicestype DevicePresenceAggregator struct { strategy AggregationStrategy} type AggregationStrategy int const ( MostPresentWins AggregationStrategy = iota PrimaryDeviceWins LastActiveWins) // AggregatePresence computes the displayed presence from all device connectionsfunc (a *DevicePresenceAggregator) AggregatePresence(connections []ConnectionInfo) PresenceState { if len(connections) == 0 { return StateOffline } switch a.strategy { case MostPresentWins: return a.mostPresentWins(connections) case PrimaryDeviceWins: return a.primaryDeviceWins(connections) case LastActiveWins: return a.lastActiveWins(connections) default: return a.mostPresentWins(connections) }} // mostPresentWins returns the highest-priority presence statefunc (a *DevicePresenceAggregator) mostPresentWins(connections []ConnectionInfo) PresenceState { // Priority order: DND > Online > Idle > Away // DND wins because explicit user intent to not be disturbed // Online > Idle because user is active somewhere var hasDND, hasOnline, hasIdle, hasAway bool for _, conn := range connections { switch conn.State { case StateDND: hasDND = true case StateOnline: hasOnline = true case StateIdle: hasIdle = true case StateAway: hasAway = true } } // DND is an explicit choice - it should win if hasDND { return StateDND } if hasOnline { return StateOnline } if hasIdle { return StateIdle } if hasAway { return StateAway } return StateOffline} // primaryDeviceWins uses the most recently active devicefunc (a *DevicePresenceAggregator) primaryDeviceWins(connections []ConnectionInfo) PresenceState { if len(connections) == 0 { return StateOffline } // Find the device with most recent activity primary := connections[0] for _, conn := range connections[1:] { if conn.LastActivity.After(primary.LastActivity) { primary = conn } } return primary.State} // lastActiveWins uses the most recently active device across all devicesfunc (a *DevicePresenceAggregator) lastActiveWins(connections []ConnectionInfo) PresenceState { sorted := make([]ConnectionInfo, len(connections)) copy(sorted, connections) sort.Slice(sorted, func(i, j int) bool { return sorted[i].LastActivity.After(sorted[j].LastActivity) }) return sorted[0].State} // DeviceSpecificPresence provides per-device presence for rich display// e.g., "Online on iPhone, Idle on Desktop"type DeviceSpecificPresence struct { UserID uint64 AggregatedState PresenceState Devices []DeviceStatus LastSeenDevice string LastSeenTime time.Time} type DeviceStatus struct { DeviceType string // "desktop", "mobile", "web", "tablet" Platform string // "Windows", "macOS", "iOS", "Android" State PresenceState LastActivity time.Time} // ToDisplayString creates human-readable multi-device statusfunc (dsp *DeviceSpecificPresence) ToDisplayString() string { if len(dsp.Devices) == 0 { return "Offline" } if len(dsp.Devices) == 1 { return fmt.Sprintf("%s on %s", stateToString(dsp.Devices[0].State), dsp.Devices[0].Platform) } // Multiple devices - show abbreviated summary var online, idle int for _, d := range dsp.Devices { switch d.State { case StateOnline: online++ case StateIdle: idle++ } } if online > 0 && idle > 0 { return fmt.Sprintf("Online on %d devices", online) } return fmt.Sprintf("%s on %d devices", stateToString(dsp.AggregatedState), len(dsp.Devices))} func stateToString(state PresenceState) string { switch state { case StateOnline: return "Online" case StateIdle: return "Idle" case StateAway: return "Away" case StateDND: return "Do Not Disturb" default: return "Offline" }}Some platforms (like Slack and Discord) show device indicators: 📱 for mobile, 💻 for desktop. This lets observers understand why someone might be slower to respond (on mobile) or unable to join a call (no video on phone). Consider exposing device type alongside aggregated presence for richer context.
At Facebook or WhatsApp scale (billions of users), naive presence implementations collapse. Here are production-grade optimizations:
1. Presence Sharding:
Partition presence data by user ID hash. Each shard handles a subset of users, enabling horizontal scaling:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
package presence import ( "hash/fnv") type ShardedPresenceStore struct { shards []*PresenceStore shardCount int} func NewShardedPresenceStore(redisClients []*redis.Client) *ShardedPresenceStore { shards := make([]*PresenceStore, len(redisClients)) for i, client := range redisClients { shards[i] = NewPresenceStore(client) } return &ShardedPresenceStore{ shards: shards, shardCount: len(shards), }} func (s *ShardedPresenceStore) getShard(userID uint64) *PresenceStore { // Consistent hashing to determine shard h := fnv.New32a() h.Write([]byte(fmt.Sprintf("%d", userID))) shardIdx := h.Sum32() % uint32(s.shardCount) return s.shards[shardIdx]} func (s *ShardedPresenceStore) GetPresence(ctx context.Context, userID uint64) (*UserPresence, error) { return s.getShard(userID).GetPresence(ctx, userID)} // Batch query that fans out to relevant shardsfunc (s *ShardedPresenceStore) GetPresenceBatch(ctx context.Context, userIDs []uint64) (map[uint64]*UserPresence, error) { // Group users by shard shardUserIDs := make(map[int][]uint64) for _, userID := range userIDs { shardIdx := int(fnv.New32a().Sum32() % uint32(s.shardCount)) shardUserIDs[shardIdx] = append(shardUserIDs[shardIdx], userID) } // Query each shard in parallel var wg sync.WaitGroup results := make([]map[uint64]*UserPresence, s.shardCount) for shardIdx, ids := range shardUserIDs { wg.Add(1) go func(idx int, userIDs []uint64) { defer wg.Done() results[idx], _ = s.shards[idx].GetPresenceBatch(ctx, userIDs) }(shardIdx, ids) } wg.Wait() // Merge results merged := make(map[uint64]*UserPresence, len(userIDs)) for _, shardResult := range results { for userID, presence := range shardResult { merged[userID] = presence } } return merged, nil}2. Lazy Presence Loading:
Don't fetch presence until the user's avatar is visible. Virtualized lists (only rendering visible items) naturally achieve this:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Client-side lazy presence loadinginterface PresenceState { state: 'online' | 'idle' | 'offline' | 'dnd'; lastSeen?: Date;} class LazyPresenceManager { private cache: Map<string, PresenceState> = new Map(); private pending: Set<string> = new Set(); private batchTimer: NodeJS.Timeout | null = null; private batchDelay = 100; // ms // Called when a user becomes visible in viewport requestPresence(userIds: string[]): void { const needed = userIds.filter(id => !this.cache.has(id) && !this.pending.has(id)); if (needed.length === 0) return; needed.forEach(id => this.pending.add(id)); this.scheduleBatch(); } private scheduleBatch(): void { if (this.batchTimer) return; this.batchTimer = setTimeout(() => { this.flushBatch(); this.batchTimer = null; }, this.batchDelay); } private async flushBatch(): Promise<void> { const userIds = Array.from(this.pending); this.pending.clear(); if (userIds.length === 0) return; // Fetch from server in single batch request const response = await fetch('/api/presence/batch', { method: 'POST', body: JSON.stringify({ userIds }), }); const presenceData: Record<string, PresenceState> = await response.json(); // Update cache for (const [userId, presence] of Object.entries(presenceData)) { this.cache.set(userId, presence); } // Notify subscribers this.notifyUpdate(Object.keys(presenceData)); } getPresence(userId: string): PresenceState | undefined { return this.cache.get(userId); } // Subscribe to real-time updates subscribeToUpdates(callback: (userId: string, presence: PresenceState) => void): void { // WebSocket subscription for push updates }}3. Presence Compression:
For systems with millions of users per channel (like public Discord servers), bitmap-based presence is more efficient than individual entries:
Channel presence: 1 bit per user → 125KB for 1 million users
Vs. Set of user IDs: ~8 bytes per user → 8MB for 1 million users
4. Hierarchical Presence:
Aggregate presence at org/team/channel level before individual level:
5. Presence Expiry with Active Renewal:
Instead of explicit offline events, use automatic expiry:
WhatsApp famously doesn't show real-time presence by default (just 'last seen'). This is partly a privacy feature, but it also dramatically reduces presence traffic. When a 1:1 conversation is opened, only then does it fetch and subscribe to that specific user's presence—a perfect example of lazy presence in action.
Presence data is sensitive. It reveals when users are active, their activity patterns, and potentially their location (if showing device type). Privacy controls are essential:
Privacy Features to Implement:
| Feature | Description | Implementation |
|---|---|---|
| Invisible Mode | User appears offline but can still use the app | Set visible_state to Offline while maintaining actual state |
| Last Seen Privacy | Hide when user was last active | Only return last_seen to approved contacts, or disable entirely |
| Read Receipts | Hide that user viewed content | Separate from presence but often coupled in UI |
| Selective Presence | Different status for different groups | Per-contact or per-channel visibility rules |
| Presence Delay | Artificial delay before showing online | Return stale presence for non-close contacts |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
package presence // PrivacySettings controls what presence information is visible to whomtype PrivacySettings struct { UserID uint64 // Who can see online status OnlineVisibility VisibilityLevel // everyone, contacts, nobody // Who can see last seen timestamp LastSeenVisibility VisibilityLevel // Who can see activity (playing game, listening to music) ActivityVisibility VisibilityLevel // Blocked users never see presence BlockedUsers []uint64 // Close friends see presence even with restricted settings CloseFriends []uint64} type VisibilityLevel int const ( VisibleToEveryone VisibilityLevel = iota VisibleToContacts VisibleToCloseFriends VisibleToNobody) // PresenceVisibilityResolver determines what presence info to returntype PresenceVisibilityResolver struct { privacyStore PrivacyStore contactStore ContactStore} // ResolveVisiblePresence returns presence adjusted for privacy settingsfunc (r *PresenceVisibilityResolver) ResolveVisiblePresence( ctx context.Context, targetUserID uint64, viewerUserID uint64,) (*UserPresence, error) { // Get actual presence presence, err := r.presenceStore.GetPresence(ctx, targetUserID) if err != nil { return nil, err } // Get target's privacy settings settings, err := r.privacyStore.GetSettings(ctx, targetUserID) if err != nil { return nil, err } // Check if viewer is blocked if contains(settings.BlockedUsers, viewerUserID) { return &UserPresence{ UserID: targetUserID, State: StateOffline, VisibleState: StateOffline, }, nil } // Check visibility level canSeeOnline := r.canView(ctx, settings.OnlineVisibility, targetUserID, viewerUserID, settings) canSeeLastSeen := r.canView(ctx, settings.LastSeenVisibility, targetUserID, viewerUserID, settings) canSeeActivity := r.canView(ctx, settings.ActivityVisibility, targetUserID, viewerUserID, settings) // Build privacy-filtered presence filtered := &UserPresence{ UserID: targetUserID, } if canSeeOnline { filtered.State = presence.VisibleState // Use visible_state, not actual state filtered.VisibleState = presence.VisibleState } else { filtered.State = StateOffline filtered.VisibleState = StateOffline } if canSeeLastSeen && presence.State == StateOffline { filtered.LastSeen = presence.LastSeen } if canSeeActivity { filtered.ActivityType = presence.ActivityType filtered.ActivityName = presence.ActivityName filtered.StatusText = presence.StatusText filtered.StatusEmoji = presence.StatusEmoji } return filtered, nil} func (r *PresenceVisibilityResolver) canView( ctx context.Context, level VisibilityLevel, targetUserID, viewerUserID uint64, settings *PrivacySettings,) bool { switch level { case VisibleToEveryone: return true case VisibleToContacts: return r.contactStore.AreContacts(ctx, targetUserID, viewerUserID) case VisibleToCloseFriends: return contains(settings.CloseFriends, viewerUserID) case VisibleToNobody: return false default: return false }} func contains(slice []uint64, val uint64) bool { for _, v := range slice { if v == val { return true } } return false}In jurisdictions with strong privacy laws (EU, California), presence data may be considered personal data. Ensure you provide mechanisms for users to control their presence visibility, understand what's collected, and request deletion. 'Last seen' timestamps, in particular, have been subject to privacy complaints.
Let's examine how major platforms implement presence:
Slack:
Discord:
WhatsApp:
Microsoft Teams:
| Platform | Push/Pull | Multi-Device | Rich Status | Privacy Controls |
|---|---|---|---|---|
| Slack | Hybrid (push to active, poll otherwise) | Primary device wins | Custom status + expiry | Per-workspace |
| Discord | Push (WebSocket) | Most-present-wins | Game activity, Spotify, streaming | Invisible mode |
| Lazy pull (on chat open) | Any device = online | Minimal (just online/offline) | Granular last seen | |
| Teams | Push (SignalR) | Calendar-aware | Calendar integration, OOF | Enterprise admin controls |
| Zoom | Push (when in meeting view) | Meeting presence only | In meeting / screen sharing | Limited |
Notice that each platform tailors presence to its use case. Slack optimizes for workplace collaboration (calendar integration, workspace-scoping). Discord optimizes for gaming communities (game activity, streaming status). WhatsApp optimizes for privacy (minimal, lazy presence). Design your presence system for your specific user context.
Presence systems are foundational to real-time applications, providing the "is anyone there?" signal that makes digital communication feel human.
You now understand how to design and implement presence systems at scale. Next, we'll explore Real-Time Collaboration—the techniques that power Google Docs, Figma, and other multi-user editing experiences, including operational transformation and CRDTs.