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.
Medium because undebounced typing events generate excessive message volume that inflates server load and costs in proportion to the active user count.
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);
});
ID: community-realtime.presence.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) || []) });
});