Skip to content
Piyush ๐Ÿ‘‹
all writing

Group chat on REST and polling. No WebSockets. (On purpose.)

Dec 2025 ยท 6 min read

Every group-chat tutorial reaches for WebSockets on step one. I built mine โ€” chat-b (FastAPI) plus chat-f (Vite + React) โ€” on plain REST and polling, and I'd make the same call again for anything under a hundred concurrent chatters.

The API is four endpoints and a shrug: POST /api/messages takes {text, sender} and returns the timestamped, id-stamped message; GET /api/history?limit&before_id pages backward through time; GET /api/health exists so deploys can prove they're alive; GET / says "Chat Server Running (REST)" โ€” the parens doing honest work. Pydantic models guard the boundary, a tiny storage module owns persistence, and CORS is wide open because frontend and backend deploy separately.

Why polling wins here: both halves deploy to serverless-friendly hosts where long-lived socket connections are the thing that breaks. Polling is stateless, cacheable, debuggable with curl, and survives a deploy mid-conversation. The pagination cursor (before_id) means the client never re-fetches the world โ€” new messages poll, old messages page. For a side-project chat room, that's the entire scaling story, and it's enough.

The idea I stole: chat UIs are just an append-only log with good scrolling. Timestamps, sender rails, auto-scroll-to-bottom, unreadable-history-above. Get the log semantics right and the "chat" feeling is free.

When would I upgrade? The day a room wants typing indicators, presence dots, or sub-second delivery guarantees โ€” that's the WebSocket-shaped hole. The migration path is clean because the message model already exists: keep REST for history, add sockets for live fan-out. Both transports, one log. Until then, polling is a feature: fewer moving parts, fewer 3am pages.