Projects
Jan 7, 2026

Online/Offline Indicator System

Distributed presence tracking using Memcached

The online/offline status system is a real-time feature that tells users which contacts are currently available. This article explains how presence is tracked across a distributed, multi-server architecture using Memcached as the central session store.

Overview

Instead of storing user status in the database (which would require constant updates and queries), we use Memcached, a fast, in-memory cache system, to track active user sessions. Each time a user connects, we create an entry with a TTL of 60 seconds. When they disconnect, the TTL expires and the entry gets removed.

Why Memcached Instead of Database?

ApproachProsCons
DatabasePersistent, queryableSlow, frequent updates, storage overhead
MemcachedUltra-fast, perfect for transient dataData lost on server restart (acceptable for online status)
RedisFast + persistence optionsSlightly heavier than Memcached for this use case

For presence status, Memcached is ideal because:

  • Presence data is transient (valid only while user is online)
  • We need sub-millisecond response times
  • Persistence isn't required (presence resets on disconnect)
  • Memory efficiency is important for millions of sessions

Architecture

Presence System Architecture

Implementation Details

Presence Key Structure

Format: user:{userId}:online
Example: user:123:online
Value: { serverId: 'server-1', connectedAt: 1234567890 }
TTL: 60 seconds (with heartbeat refresh)

Connection Flow

// When user connects via WebSocket
socket.on('connect', async () => {
  const userId = socket.data.userId // From JWT
  const serverId = process.env.SERVER_ID // e.g., 'server-1'

  // Create entry in Memcached
  const sessionKey = `user:${userId}:online`
  await memcached.set(
    sessionKey,
    {
      serverId,
      connectedAt: Date.now(),
      socketId: socket.id,
    },
    60 // 60 second expiration
  )
})

Heartbeat Mechanism

To keep sessions alive, we use a heartbeat. Since we are using Socket.IO, we can utilize the inbuilt Socket.IO ping events as heartbeats to refresh presence status in Memcached. If we refresh the status on Memcached on every ping, then Memcached will be overwhelmed with queries. So we accumulate all pings from all users for 5 seconds and then perform a batch refresh.

const pingTrackingSet = new Set<number>()

function setupPingTracking(socket: Socket, userId: number) {
  // Monitor socket.io ping packets
  socket.conn.on('packet', (packet: { type: string }) => {
    if (packet.type === 'pong') {
      pingTrackingSet.add(userId)
    }
  })
}

function getAndClearPingTrackingSet(): number[] {
  const userIds = Array.from(pingTrackingSet)
  pingTrackingSet.clear()
  return userIds
}

// Start periodic flush of ping tracking to memcached
setInterval(async () => {
  try {
    const userIds = getAndClearPingTrackingSet()
    if (userIds.length > 0) {
      await memcachedService.setBatchOnline(userIds, ONLINE_STATUS_TTL)
    }
  } catch (error) {
    console.error('Error flushing ping tracking to memcached:', error)
  }
}, PING_FLUSH_INTERVAL_MS) // 5 seconds

Disconnection Flow

When a user disconnects, we simply ignore. Since the user has disconnected, no heartbeats will arrive from the user, and the Memcached entry won't be refreshed. Hence the presence entry will expire after the TTL, that is, 60 seconds.

Presence Synchronization

The client "pulls" the presence status of the visible users periodically. Also if the document is not visible on screen, for example if the user has minimized the screen, then we pause the "pulls". Document visibility is detected with the Page Visibility API.

Online/Offline Status Flow