Live Classroom Q&A and Presence with NATS
An education use case: a virtual classroom chat/Q&A subject per session, plus a lightweight presence pattern (who's currently online) built from core pub/sub and a KV heartbeat bucket.
- nats
- education
- chat
- presence
- tutorial
The scenario
A virtual classroom platform needs: a live chat/Q&A stream per class session, and a "who's currently here" roster the teacher can glance at — without a database round-trip on every keystroke.
Step 1 — one subject per class session
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:latest
nats context add local --server="nats://localhost:4222" --selectnats sub "class.101.chat"nats pub class.101.chat '{"from":"student-a","text":"Is the quiz open-book?"}'nats pub class.101.chat '{"from":"teacher","text":"Yes, notes are allowed."}'Every message — question or answer — reaches everyone subscribed to class.101.chat, in order, the
instant it's sent.
Step 2 — many simultaneous sessions, no cross-talk
Each session gets its own subject; a school running many classes at once needs zero extra infrastructure, since subjects are just strings:
nats sub "class.205.chat"nats pub class.205.chat '{"from":"student-c","text":"Can we get an extension?"}'The message never reaches the class.101.chat subscriber — subjects are isolated unless a
subscription's wildcard deliberately spans them (e.g. class.*.chat for a school-wide moderation
dashboard).
Step 3 — presence: "who's here right now"
Chat messages alone don't tell you who's silently listening. Add a lightweight heartbeat pattern: each client periodically writes its own presence key with a short TTL-like convention, and a KV bucket holds the current roster.
docker rm -f nats-server
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 -v nats-js-store:/data nats:latest -js -sd /datanats kv add CLASS_101_PRESENCE --ttl=30snats kv put CLASS_101_PRESENCE student-a '{"joinedAt":"2026-07-24T09:00:00Z"}'
nats kv put CLASS_101_PRESENCE student-b '{"joinedAt":"2026-07-24T09:00:05Z"}'nats kv ls CLASS_101_PRESENCEBecause the bucket was created with --ttl=30s, any student key not refreshed within 30 seconds
expires automatically — so a student who closes their laptop without saying goodbye silently drops off
the roster, no explicit "leaving" message required.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Students in different sessions see each other's chat | Both published to the same literal subject (e.g. hardcoded class.chat) | Always interpolate the actual session id into the subject, e.g. class.101.chat |
| Presence roster shows students who already left | Client's heartbeat loop stopped but bucket TTL hasn't elapsed yet | Expected — TTL is an upper bound; shorten --ttl for faster staleness detection, but weigh it against kv put traffic volume |
nats kv add ... --ttl=30s errors with "JetStream not enabled" | Server started without -js | Recreate the container with -js -sd /data as shown in Step 3 |
| Chat messages arrive out of order across two subscribers | Expected in distributed delivery in general, but core NATS preserves per-publisher order to a single subscriber | If strict global ordering across all publishers matters, use a JetStream stream with a single consumer instead of plain pub/sub |
Recap
| Concept | Takeaway |
|---|---|
class.<session-id>.chat | One subject per session keeps classes isolated with zero extra infra |
class.*.chat | A moderation/school-wide dashboard can watch every session at once via wildcard |
KV bucket with --ttl | Cheap, self-cleaning presence roster — no explicit "user left" event needed |