| Choice | Presence accuracy | Traffic pattern | Best fit |
|---|---|---|---|
| Broadcast typing events | Weak proxy for attendance | Bursty and tied to keystrokes | Text composition where typing itself matters |
| Skip typing events | No false presence signal | None | Polls that already track joins and answers |
| Expiring intent signal | Direct, time-bounded poll intent | One start, sparse refresh, one stop | Live polls where facilitators need readiness |
TL;DR: For a live health session poll, skip typing indicators over the shared channel. Skipping them removes misleading chatter; track an expiring engaged intent separately from connection presence, and count only accepted answers as participation. This gives the facilitator useful readiness without pretending that keyboard activity proves a participant is present.
My decision rule is blunt: if nobody needs to react to the act of typing, don't publish it. A poll needs a stable denominator, an auditable answer count, and a clear timeout. Keystrokes provide none of those.
Should a poll channel carry typing indicators or skip them entirely?
A typing event answers a narrow question: did this client observe recent input? The session host is asking a different one: who is still eligible to answer? Those states split quickly. A participant can read for 20 seconds without touching the keyboard. A mobile keyboard can open and close without an answer. A browser can reconnect after the last stop event disappeared. Treating any of those transitions as attendance turns a UI hint into a source of truth. Typing indicators belong on a channel only when another person can act on the composition itself; this poll doesn't meet that test.
That's the trap.
For a health coaching session, I would define three independent states: connected, engaged with this poll, and answered. Connection is transport-level and may flicker. Engagement is a short lease created by opening or interacting with the poll. Answered is a server-accepted domain event. The host UI can display all three, but it must not collapse them into one green dot.
WebRTC reinforces this separation. Its specification defines peer connections and data channels, including data-channel lifecycle and delivery behavior. It does not define application meaning for a typing message. The application owns that contract, including expiry, ordering, privacy, and what happens after reconnection. That ownership is a real limitation of the intent-lease approach: every client and receiver must agree on sequence handling and expiry, and the team must test those rules under interruption. A basic connection-and-answer display has less application state to maintain.
Criterion one: measure the state the facilitator can act on
Suppose a session has 12 invited participants. The useful display is not “5 people are typing.” It is “10 connected, 8 opened the poll, 6 answers accepted.” Those are example UI counts, not benchmark results. Each number maps to a decision: wait for reconnections, prompt people who have not opened the poll, or move on when enough valid answers arrive.
Typing chatter muddies that model because it expires locally and often lacks a durable transition. If the host misses typing:stop, the indicator lies until a timer repairs it. If the timeout is too short, slow readers blink in and out. If it is long, stale intent lingers. No timeout fixes the semantic mismatch.
Use a lease only for the state that is genuinely temporary. An engaged signal can expire after 15 seconds and refresh no more than once every 5 seconds. Those values are design parameters to test with the actual session cadence, not universal constants. The accepted answer remains server-authoritative and survives that lease ending.
Accuracy comes from separate clocks and separate meanings. Presence answers “can we currently reach this participant?” Engagement answers “has this participant touched this poll recently?” Submission answers “did the system accept a response?”
Criterion two: bound chatter before optimizing it
I benchmark event budgets before debating transports. Raw message latency looks scientific, but it can hide a bad event model. Start with an upper bound that can be checked from logs.
For example, a client that sends one engagement start, refreshes at most twice during a short poll, and sends one explicit end produces at most four intent messages per poll in the normal path. A per-keystroke design has no similarly useful bound because input length and correction behavior drive the count. Debouncing reduces traffic, yet it still reports keyboard behavior rather than poll readiness.
The payload should stay boring: poll ID, anonymous session-scoped participant ID, monotonically increasing sequence, state, and client timestamp. The receiver uses the sequence to reject older updates and its own receipt time to enforce the lease. Never let client time decide whether an answer was accepted.
No prose goes in it.
This is also a privacy boundary. A readiness flag reveals less behavior than a stream shaped by pauses and edits. It still reveals activity, so retain it briefly, avoid clinical content in the payload, and keep it out of the durable answer record unless there is a documented reason to store it.
A small protocol that survives reconnects
The transport can be a WebRTC data channel or another bidirectional channel. Keep the application contract transport-neutral. This TypeScript sketch shows the receiver-side rule; it intentionally does not include framework setup or vendor glue.
type IntentEvent = { pollId: string; participantId: string; sequence: number; state: "engaged" | "left"; sentAtMs: number; }; type IntentLease = { sequence: number; expiresAtMs: number; }; const leases = new Map<string, IntentLease>(); const LEASE_MS = 15_000; function applyIntent(event: IntentEvent, receivedAtMs: number): void { const key = `${event.pollId}:${event.participantId}`; const current = leases.get(key); if (current && event.sequence <= current.sequence) return; if (event.state === "left") { leases.delete(key); return; } leases.set(key, { sequence: event.sequence, expiresAtMs: receivedAtMs + LEASE_MS, }); } function isEngaged(key: string, nowMs: number): boolean { const lease = leases.get(key); return lease !== undefined && lease.expiresAtMs > nowMs; } A reconnect starts a new sequence epoch or obtains a server-issued session token; otherwise a delayed event from the old connection can overwrite current state. The exact mechanism depends on the surrounding identity model. The invariant does not: old intent must never revive an expired participant. Consider a phone that loses connectivity after sending sequence 8, reconnects, and emits sequence 1 while sequence 8 is still buffered elsewhere. Comparing bare counters would discard the new session as stale. Comparing only arrival time could let the old session win later. A connection-scoped epoch makes the ordering explicit, while the lease ensures either version disappears without a final event.
Make stale state die.
Test the protocol with reordered events, a missing left, duplicate refreshes, background-tab suspension, and reconnect. Then observe counts, not payload bodies: accepted events, stale-sequence drops, lease expirations, and the gap between connected and engaged participants. Alerting on every expired lease would be noise. A sustained, session-wide divergence is worth investigation.
When is the runner-up actually better?
Broadcast typing events are the runner-up, and they win when typing is itself the shared interaction. A moderated text chat is the obvious case: seeing that a clinician or participant is composing can prevent conversational collisions. Even there, the signal should be ephemeral, rate-limited, scoped to the conversation, and excluded from attendance or response metrics. This is the main trade-off: replacing familiar typing events with an intent lease improves semantic fit for the poll but creates a new state contract that SDKs, tests, dashboards, and privacy review must understand.
There is another legitimate choice: skip all intent signals. Do that when the host cannot take a useful action before answers arrive, when the poll is very short, or when activity metadata creates more privacy review than product value. The interface can show connection count and accepted responses. Less state means fewer ambiguous transitions and less configuration.
For the live poll, the expiring intent signal earns its place only if facilitators really use “opened but not answered” to pace the session. Instrument that decision. If the display changes nothing, delete the signal. I would rather remove an event than tune five knobs around a question nobody acts on.

