Short answer: use typing indicators over a channel only when they prevent a real coordination mistake; otherwise, skipping them is the better default. For a shared developer workspace, the deciding constraint is fan-out: one person's keystrokes can create work for every open tab in a room, while a missed indicator changes no durable state. Give each useful signal a 2-second expiry and treat it as lossy presence, never as a reliable message stream.
The useful question is not whether the animation feels lively. It is whether seeing "Sam is typing" changes what another person does. In a two-person support thread, perhaps it prevents a duplicate reply. In a 40-person project channel, it may add motion without adding information. Silence is a valid design.
Replace keystroke chatter with a short lease
The before model is easy to picture: key down, publish; key down, publish; key down, publish. Every subscriber receives a trail of events whose meaning disappears almost immediately. A delayed "typing" event can even arrive after the message it was meant to foreshadow.
The after model has three moments. A client crosses from idle to active and emits one start signal. The server fans out a tiny lease with an expiry. A stop signal may clear it early, but receivers also remove it when the lease expires. Start, refresh, vanish. That is the whole diagram in words. Use a fixed interaction budget rather than pretending the signal is durable. A 2-second lease is concrete enough to test and short enough that an abandoned tab does not leave a permanent label. Refresh once near the middle of the lease while input continues; do not publish every keypress. The exact interval is a product choice, so validate it with users instead of presenting 2 seconds as a standard. This creates a clean delivery rule: the newest state for one member supersedes older state, duplicates are harmless, and late events are rejected by their expiry.
No replay. No history.
A copyable Node.js state machine
The payload needs identity, scope, and time. It does not need the draft text. Keeping content out of the signal reduces both exposure and accidental coupling between the editor and the presence layer.
type TypingSignal = {
workspaceId: string;
channelId: string;
memberId: string;
sequence: number;
active: boolean;
expiresAtMs: number;
};
type VisibleTyping = Map<string, TypingSignal>;
export function applyTypingSignal(
visible: VisibleTyping,
signal: TypingSignal,
nowMs: number,
): VisibleTyping {
const next = new Map(visible);
const previous = next.get(signal.memberId);
if (previous && signal.sequence <= previous.sequence) return next;
if (!signal.active || signal.expiresAtMs <= nowMs) {
next.delete(signal.memberId);
return next;
}
next.set(signal.memberId, signal);
return next;
}
export function removeExpired(
visible: VisibleTyping,
nowMs: number,
): VisibleTyping {
return new Map(
[...visible].filter(([, signal]) => signal.expiresAtMs > nowMs),
);
}
The sequence number handles reordering within one member's session. The expiry handles the less tidy endings: a laptop sleeps, a tab closes, or a stop signal never arrives. Keep the server authoritative about the maximum accepted expiry so a client cannot request an unlimited lease. Also exclude the sender from fan-out; the local editor already knows that it is active.
There is a tempting mistake here. Reliable delivery sounds safer, so teams may place typing events behind the same retries and ordered queue used for messages. That preserves stale intent. A retry that arrives after expiry is worse than a dropped update because it displays a state that is no longer true. Durable chat messages and ephemeral typing signals deserve separate delivery policies.
Should a channel show typing indicators or skip them entirely?
Run a small decision table before building transport. The honest outcome may be skipping typing indicators entirely.
| Situation | User value | Fan-out shape | Decision |
|---|---|---|---|
| Two people composing replies in one thread | Can reduce duplicate responses | One signal reaches one peer | Use a lease |
| Busy project room with many passive viewers | Weak and visually noisy | Each transition reaches many tabs | Skip them |
| Structured form or code review with explicit ownership | Existing workflow already shows intent | Extra transient stream | Skip them entirely |
| Small incident room where response collision matters | Coordination can change quickly | Bounded, active audience | Consider a lease |
This is an interaction trade-off, not a transport contest. Measure transitions emitted per active composer, recipients per transition, expired states removed locally, and signals rejected as late. Then pair those operational metrics with one behavioral question: did two people begin the same response less often? Delivery volume alone cannot establish value.
Alert on broken semantics, not ordinary loss. A rising count of leases surviving past their expiry indicates client cleanup trouble. A large late-rejection rate points to delay or clock-policy problems. A missing individual signal is expected under the model.
WebRTC data channels add one more transport option.
A peer-to-peer data path can look attractive because typing is transient. WebRTC data channels support configurable ordering and retransmission behavior, and the specification describes both reliable and partially reliable modes. That maps well to an expiring signal: an application can avoid insisting that every obsolete update be delivered.
But transport does not erase topology. In a shared workspace, each participant still needs the right peers, authorization, lifecycle handling, and a consistent view of membership. A server-mediated channel is often easier to reason about when rooms are large or membership changes frequently; a peer path may fit an already-established small session. This is a design distinction, not a recommendation for one implementation.
Whichever path carries the event, preserve the same application contract: bounded expiry, monotonic sequence per sender session, no draft content, and no durable replay. The contract keeps the UI correct when the transport duplicates, reorders, delays, or drops information within its configured behavior.
What if users expect immediate feedback?
Immediate local feedback and remote fan-out are different jobs. The composer can see a local active state with zero network traffic. Remote peers should see a signal only after a short debounce confirms actual composition; brief focus changes and single accidental keystrokes do not need to become room-wide events.
Test the ugly paths. Deliver the same start twice. Deliver stop before an older start. Advance time beyond 2 seconds without stop. Disconnect the sender. Open two tabs for one member. A passing test does not assert that every packet arrived; it asserts that no indicator outlived its lease and no older sequence replaced a newer one.
Ship the feature only if the coordination benefit survives that discipline. Otherwise, omit it and keep durable messages, assignments, and explicit presence as the signals users can trust. Less motion can be clearer.
References
- W3C, WebRTC 1.0: Real-Time Communication Between Browsers: https://www.w3.org/TR/webrtc/
Top comments (0)