Issue
When a client unsubscribes from a channel, RealtimeChannel.terminate/2 doesn't call UsersCounter.remove. This means the user stays in the census until the idle socket sweeper (Tracker) eventually kills the connection, which can take up to 10 minutes.
I found this while debugging an SPA where users frequently switch between channels. The connected user count kept increasing because users weren't being removed when they left a room, eventually causing "Too many connected users" errors.
Consequences
- Unnecessary Postgres connections: Ghost users can keep tenant Postgres pools alive for up to 10 minutes, potentially wasting connections.
- Customer friction: Stale users can consume
max_concurrent_users slots, causing customers to hit their limits sooner and trigger rate limiting.
To Reproduce
- Open a WebSocket without joining a channel — uses 0 user slots.
- Join a channel —
UsersCounter.add/2 is called, using 1 slot.
- Call
channel.unsubscribe() — RealtimeChannel.terminate/2 runs.
- Bug:
UsersCounter.remove is never called. The user has no active channels but still counts as 1 user for up to 10 minutes.
Proposed Fix
Remove the user from UsersCounter when their active channel count drops to 0 in terminate/2:
if Tracker.untrack(transport_pid) <= 0 do Realtime.UsersCounter.remove(transport_pid, tenant_id) end
Issue
When a client unsubscribes from a channel,
RealtimeChannel.terminate/2doesn't callUsersCounter.remove. This means the user stays in the census until the idle socket sweeper (Tracker) eventually kills the connection, which can take up to 10 minutes.I found this while debugging an SPA where users frequently switch between channels. The connected user count kept increasing because users weren't being removed when they left a room, eventually causing
"Too many connected users"errors.Consequences
max_concurrent_usersslots, causing customers to hit their limits sooner and trigger rate limiting.To Reproduce
UsersCounter.add/2is called, using 1 slot.channel.unsubscribe()—RealtimeChannel.terminate/2runs.UsersCounter.removeis never called. The user has no active channels but still counts as 1 user for up to 10 minutes.Proposed Fix
Remove the user from
UsersCounterwhen their active channel count drops to 0 interminate/2:if Tracker.untrack(transport_pid) <= 0 do Realtime.UsersCounter.remove(transport_pid, tenant_id) end