Skip to content

ADR-0008 — fakeUserOnline inflates the online counter

Status: Accepted Date: 2026-04-16 Author(s): Platform engineering

Context

The player-facing UI displays a "live players" counter updated every 10 seconds via the Server.UsersOnlineUpdated websocket event. The real online count comes from ONLINE_USERS_KEY, a Redis sorted set where members are user IDs and scores are epoch-ms of last activity. The set is swept every minute by zremrangebyscore with a 30-second TTL (ONLINE_USER_TTL_SECONDS at apps/rt/src/online-tracker/const.ts:3).

On a quiet environment — local dev, staging, or off-peak production — the real zcard returns single digits. Product requires the counter to "look healthy" rather than show the true value.

Decision

OnlineTrackerService at apps/api/src/user/online-tracker.service.ts:18 maintains a fakeUserOnline variable:

  • Initial value: 500
  • Drift: every 10 seconds, fakeUserOnline += Math.floor(Math.random() * 10 - 5) — a random walk of −5 to +4 per tick
  • Floor: clamped to minimum 180 (line 31-33)
  • Broadcast: the cron at line 20 (@Evo.Cron(CronExpression.EVERY_10_SECONDS)) emits UsersOnlineUpdated with value: zcard(ONLINE_USERS_KEY) + fakeUserOnline

The getOnlineCount() method at line 9 returns the real zcard without inflation. Only the websocket broadcast adds the fake count.

Alternatives considered

  1. Show the real count. Rejected by product: a counter showing "3 players online" undermines user confidence. The inflation is a deliberate product decision, not an engineering hack.

  2. Use a fixed floor (e.g., always add 500). Rejected: a static number looks artificial. The random walk makes the counter wiggle naturally — viewers see it drift up and down, which reads as organic activity.

  3. Seed bot users into the ONLINE_USERS_KEY zset. Rejected: pollutes the real online-user data. Bot entries would appear in admin dashboards, analytics queries, and any code that iterates zset members. Keeping the inflation in the broadcast layer means the underlying data stays clean.

  4. Server-side rendered count with client-side animation. Rejected: the counter must stay in sync across all connected clients. A server-authoritative broadcast is the simplest way to ensure consistency.

Consequences

  • Any reliability or scale analysis using the UsersOnlineUpdated payload is measuring inflated data. The real count is only available via OnlineTrackerService.getOnlineCount() or redis-cli -a cache ZCARD online_users.
  • The counter never drops below 180, even with zero real users. If a stakeholder asks "why doesn't the count go to zero overnight?" — this is why.
  • Grafana panels graphing the websocket-emitted count show the inflated value. To graph real usage, add a Prometheus gauge around getOnlineCount() or scrape the zset directly.
  • Bot detection or abuse monitoring that uses the broadcast count will produce false signals. Always use the raw zcard for security or analytics purposes.
  • The enable_coming_soon feature flag (line 24-26) suppresses the broadcast entirely when enabled — the counter disappears from the UI rather than showing zero.

References

  • apps/api/src/user/online-tracker.service.ts:18fakeUserOnline = 500
  • apps/api/src/user/online-tracker.service.ts:20-46notifyOnlineUsers cron
  • apps/api/src/user/online-tracker.service.ts:9-11getOnlineCount() (real, uninflated)
  • apps/rt/src/online-tracker/const.ts:1,3ONLINE_USERS_KEY, ONLINE_USER_TTL_SECONDS
  • libs/shared/src/events/api.events.ts:11Server.UsersOnlineUpdated event