Typing indicators debounced at client and expire server-side on timeout
Why it matters
A typing indicator fired on every keystroke with no debounce generates roughly one WebSocket event per 100ms per active user. For a community with 100 simultaneous typists, that is 1,000 events per second — before any real messages are sent. Without server-side expiry, users who close their browser mid-compose are shown as typing indefinitely. Both failure modes inflate bandwidth, CPU cost, and connection noise for zero user-visible benefit, violating ISO 25010 performance-efficiency requirements.
Severity rationale
Medium because undebounced typing events generate excessive message volume that inflates server load and costs in proportion to the active user count.
Remediation
Debounce the client-side typing event to at most one emission per 500 ms, and expire server-side typing state after 10 seconds without an update.
// Client: debounce before emitting
let stopTimer: ReturnType<typeof setTimeout>;
const onInput = () => {
socket.emit('typing', { channel });
clearTimeout(stopTimer);
stopTimer = setTimeout(() => socket.emit('typing_stop', { channel }), 3_000);
};
input.addEventListener('input', onInput);
// Server: expire stale typing status
const typingTimers = new Map<string, ReturnType<typeof setTimeout>>();
socket.on('typing', ({ channel }) => {
if (typingTimers.has(socket.userId)) clearTimeout(typingTimers.get(socket.userId)!);
const t = setTimeout(() => {
removeTypingUser(channel, socket.userId);
broadcastTypingList(channel);
}, 10_000);
typingTimers.set(socket.userId, t);
addTypingUser(channel, socket.userId);
broadcastTypingList(channel);
});
Detection
-
ID:
typing-indicator-debounce -
Severity:
medium -
What to look for: Count all typing indicator event paths. Enumerate: client-side debounce mechanism, server-side expiry timeout. Quote the actual debounce interval and timeout values if found.
-
Pass criteria: Typing events are debounced on the client at an interval of at least 200ms, and the server removes typing status after a timeout of no more than 30 seconds without an update.
-
Fail criteria: Typing events are sent on every keystroke, or server doesn't expire typing status.
-
Skip (N/A) when: The platform does not implement typing indicators.
-
Detail on fail:
"Typing indicator sent on every keystroke without debounce. Client sends 100+ messages per second." -
Remediation: Implement client-side debounce and server-side expiry:
// Client: Debounce typing events let typingTimeout: NodeJS.Timeout; const debounceTyping = () => { clearTimeout(typingTimeout); socket.emit('typing', { channel: 'general' }); typingTimeout = setTimeout(() => { socket.emit('typing_stop', { channel: 'general' }); }, 3000); }; input.addEventListener('input', debounceTyping); // Server: Expire typing status const typingUsers = new Map<string, Set<string>>(); const typingTimers = new Map<string, NodeJS.Timeout>(); socket.on('typing', (data) => { if (!typingUsers.has(data.channel)) { typingUsers.set(data.channel, new Set()); } typingUsers.get(data.channel)!.add(socket.userId); // Clear old timer if (typingTimers.has(socket.userId)) { clearTimeout(typingTimers.get(socket.userId)!); } // Set new timeout to remove typing status const timer = setTimeout(() => { typingUsers.get(data.channel)?.delete(socket.userId); io.to(data.channel).emit('typing_update', { typing: Array.from(typingUsers.get(data.channel) || []) }); }, 10000); typingTimers.set(socket.userId, timer); io.to(data.channel).emit('typing_update', { typing: Array.from(typingUsers.get(data.channel) || []) }); });
External references
- iso-25010:2011 · performance-efficiency.resource-utilization — Resource utilization
Taxons
History
- 2026-04-18·v1.0.0·Initial import from community-realtime·automated