
A chat feature looks small in a ticket: “users can message each other.” In code, it is a distributed system with a UI on top. This guide walks through the parts you must build, with rough effort estimates, so you can decide if you should write them or call a chat API.
The short answer: build when messaging is the product or when you have hard constraints a vendor cannot meet. Use a chat API when chat supports a workflow like buying, booking, or support. The rest of this article shows how to check that against your own case.
The architecture of a chat feature
Every chat feature has the same six layers. Treat them as separate services, even if you ship them in one codebase.
- Transport. A live channel between client and server.
- Persistence. A store for messages, conversations, and read state.
- Fan-out. Code that sends each event to every connected participant.
- Presence. Online status and typing indicators.
- Notifications. Email and push for users who are away.
- Trust and safety. Access rules, moderation, and abuse control.
A prototype covers layers 1 to 3 on one server. A product covers all six.
Transport: WebSockets, SSE, or polling
You have three realistic options for getting a new message to the browser.
- Polling. The client asks “anything new?” every few seconds. It is simple and works behind any proxy. It wastes requests and adds latency equal to the interval.
- Server-Sent Events (SSE). One long HTTP response that streams events from server to client. Sends from the client use normal POST requests. It reconnects by itself and is easy to run.
- WebSockets. A two-way, persistent connection. The WebSocket API on MDN documents the browser side. It gives the lowest latency and suits typing indicators, but you own the connection lifecycle.
For most chat features, WebSockets are the right choice, with SSE or long polling as a fallback for networks that block them. Plan for these details:
- Heartbeats. Send a ping on an interval so you detect dead connections that never closed.
- Reconnect with backoff. Add jitter, or every client reconnects at the same second after a deploy.
- Authentication on connect. Verify a short-lived token during the handshake, not only on the first HTTP request.
- Sticky routing or a shared bus. A user on node A must receive messages sent through node B. Use a pub/sub layer such as Redis or NATS between nodes.
Estimate: 2 to 4 engineer-weeks for a gateway you would run in production, including reconnect logic and load tests.
Message persistence and ordering
Storage is where chat bugs hide. Decide these points early.
Schema and keys
A common layout has users, conversations, participants, and messages. Index messages by (conversation_id, created_at, id). Never use the client clock for ordering.
Ordering and idempotency
Two messages can arrive at the server in a different order than users sent them. Pick one source of truth for order: a per-conversation sequence number or a server-assigned time with a tiebreaker ID.
Also make sends idempotent. The client generates a message ID before sending. If the network drops and the client retries, the server sees the same ID and stores one message, not two.
Pagination and gap recovery
Load history with cursor pagination, not offset pagination. Offsets drift when new messages arrive.
When a client reconnects, it should send the last sequence number it saw. The server then replays everything after that number. Without this, users miss messages after a short network drop.
Estimate: 2 to 3 engineer-weeks for the schema, the send path, history, and replay.
Presence, typing indicators, and read state
These features create the most traffic for the least value, so design them with care.
- Presence is ephemeral. Keep it in memory or a fast store with a time-to-live, not in your main database. A user with three tabs is online until the last connection closes.
- Typing indicators should be throttled on the client (one event every few seconds) and expire on the server if no update comes.
- Read state is durable. Store a “last read” pointer per user per conversation, not a flag per message. Unread count is then a query against that pointer.
A read pointer that moves on one device must update the others. That is one more event type to fan out.
Estimate: 1 to 2 engineer-weeks.
Push notifications and offline delivery
Most replies arrive when the other person is not looking at your site. Without notifications, conversations stall.
Your notification layer needs to:
- Check if the user has an active connection before it sends anything.
- Wait a short time, then batch several messages into one email or push.
- Handle APNs and FCM tokens, including expired tokens.
- Respect per-user and per-conversation mute settings.
- Stop sending when the user reads the message on another device.
Email adds more work: templates, bounce handling, and unsubscribe rules.
Estimate: 2 to 3 engineer-weeks for email plus mobile push.
Moderation and security
Treat chat as a public input surface, even in a private app.
- Access control. Check conversation membership on every read, every send, and every subscription. A bug here exposes private messages.
- Input handling. Sanitize message content on render. Validate and scan uploaded files.
- Rate limits. Limit sends per user and per conversation to slow spam.
- User controls. Block, mute, and report. Staff need a queue to review reports and act on them.
- Data rules. If you serve EU users, plan for export and deletion of a user’s messages.
Estimate: 2 to 4 engineer-weeks, and the work does not end at launch.
Scaling
A single WebSocket node handles a lot of idle connections, but a chat system has uneven load. A popular group conversation can create a burst of fan-out to thousands of clients.
Watch these limits as you grow:
- Connection count per node and memory per connection.
- Fan-out cost for large public rooms. Consider sending lightweight events and letting the client fetch details.
- Deploys. New code drops connections. Drain nodes slowly and rely on client backoff.
The build checklist with effort
Use this as a planning sheet. The numbers are rough estimates from typical projects, not measured data, and your stack will change them.
| Component | Estimate (engineer-weeks) |
|---|---|
| Transport and reconnect | 2 to 4 |
| Persistence, ordering, history | 2 to 3 |
| Presence, typing, read state | 1 to 2 |
| Notifications (email and push) | 2 to 3 |
| Moderation and access control | 2 to 4 |
| Web UI (list, composer, uploads, accessibility) | 4 to 8 |
| Mobile UI, if you need it | 4 to 8 per platform |
| Load testing and monitoring | 1 to 2 |
That adds up to roughly 14 to 26 engineer-weeks for web only, before the first maintenance ticket. The UI is often the largest line item, and teams forget it when they say “we only need an API.”
When a chat API wins, and when building wins
A chat API wins when
- Chat helps users finish a task, such as a buyer asking a seller a question.
- You need web and mobile at the same time.
- You must ship in weeks, not quarters.
Building wins when
- Messaging is the product, and the protocol or storage engine is your edge.
- You need deployment on a network with no outside access.
- You need a custom encryption model that hosted APIs do not offer.
- Your scale makes per-user pricing higher than the cost of a dedicated team. Calculate this with real numbers before you assume it.
A short integration sketch
Providers differ, so compare more than one. TalkJS is one option, a chat API. Its SDKs cover JavaScript, React, React Native, Flutter, iOS, and Android, and it has a pre-built, customizable chat UI. Its docs show a pattern that is typical for the category: define a user, open a session, create a conversation, and render the UI.
With the JavaScript SDK, the flow looks like this. Treat it as a sketch and check the current docs for exact calls and for signing sessions on your server.
// 1. Define the current user (from your own auth system)
const me = new Talk.User({ id: "user_123", name: "Ada" });
// 2. Open a session for that user
const session = new Talk.Session({ appId: "YOUR_APP_ID", me });
// 3. Create the inbox and mount it in the page
const inbox = session.createInbox();
inbox.mount(document.getElementById("chat"));
In the React SDK, the same idea uses a Chatbox component that takes an appId, a userId, and a conversationId. You create the conversation from your own event, such as a buyer clicking “Message seller.”
Two design points apply to any provider: your backend stays the source of truth for who may talk to whom, and sessions are authenticated on your server so a user cannot act as someone else.
TalkJS also lists typing indicators and read receipts, moderation tools (flagging, blocking, a profanity filter), notifications, and webhooks. It states ISO 27001 certification, GDPR compliance, and EU hosting with regional options. Its development mode is free, which makes a two-day spike easy to run.
How to run a two-day spike
Before you commit to either route, test the risky parts.
- Build a thin vertical slice: two users, one conversation, history, and a reconnect.
- Kill the network for ten seconds on one client and check for lost or duplicated messages.
- Open three tabs and check unread counts.
- Message an offline user and check the notification.
- Try to read a conversation you do not belong to.
- Ask the vendor for the price at ten times your current user count.
If a vendor fails a step that matters to you, you have a concrete reason to build. If it passes, you saved months.
Decide with your own numbers
Chat is easy to start and hard to finish. Map your needs against the six layers, price the checklist above with your own team’s rates, and run the spike. If the work is not your product’s edge, spend your engineers on what is.