WebSocket-Based Live Support Chat with Queue and History

WebSocket-Based Live Support Chat with Queue and History

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1026
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

WebSocket-Based Live Support Chat with Queue and History

Imagine an online store with 10,000 daily visitors losing up to 30% of leads because responses take an hour. A customer leaves for a competitor that offers instant chat. An online chat on the website solves this: an operator sees the user and can help immediately. According to Statista, website chat increases conversion by 20-40%. Support costs drop by 30-50% thanks to automation of typical questions and reduced response time. But implementation is more complex than plugging in a ready-made script—it requires a robust architecture that handles thousands of concurrent connections, manages a queue, and never loses messages during disconnects. We design and deploy such systems for e-commerce, SaaS, and fintech. Our experience includes over 10 projects of similar complexity, and we guarantee stable operation under loads of up to 10K concurrent sessions.

We take on the full cycle: from WebSocket server design to deployment and monitoring. Below are technical details, code, and timelines.

At the core is a WebSocket server on Node.js and Socket.IO. This choice is driven by performance and built-in reconnection mechanisms.

Why WebSocket Instead of Long Polling?

Long Polling creates unnecessary overhead: each request generates a new HTTP packet with headers. WebSocket maintains a persistent connection, reducing traffic by 10x or more and latency down to 50 ms—6x faster than Long Polling. For a chat with a large audience, this is critical.

Feature WebSocket Long Polling
Latency ~50 ms ~300 ms+
Traffic per message ~100 bytes ~800 bytes (with headers)
Scaling Redis adapter, Nginx round-robin, more complex
Automatic reconnect Built-in Requires implementation

In practice, the difference is noticeable at 100+ concurrent sessions: WebSocket consumes 60% less CPU resources.

How to Ensure Stable WebSocket Connection During Disconnects?

Socket.IO (see documentation) provides built-in mechanisms: heartbeat, timeouts, exponential backoff. We configure pingInterval and pingTimeout for your project's load. On connection loss, the client automatically reconnects, and the server restores the chat context. Message history is stored in the database, so no message is lost.

What Happens When the Server Crashes?

Socket.IO maintains state in Redis. Upon restart, the server restores sessions. Undelivered messages are saved in the database and sent later.

Example of scaling to 10K connections

Scaling WebSocket to 10K connections is achieved with a Redis adapter and Nginx. Each server handles up to 1000 connections; when exceeded, new instances are added. Monitoring via Prometheus+Grafana.

Architecture and Tech Stack

The system consists of three parts: a server on Node.js + Socket.IO, a visitor widget (React), and an operator panel. Data is stored in PostgreSQL with indexes on chat_id and sender_type.

Backend: Node.js + Socket.IO

import { Server } from 'socket.io'; import { createServer } from 'http'; const io = new Server(createServer(), { cors: { origin: process.env.FRONTEND_URL, credentials: true }, }); // Operator authorization io.use(async (socket, next) => { const token = socket.handshake.auth.token; if (token) { const user = await verifyToken(token); if (user?.role === 'operator') { socket.data.user = user; socket.data.isOperator = true; } } next(); }); io.on('connection', (socket) => { const { chatId, isOperator } = socket.data; // Visitor starts a chat socket.on('chat:start', async (data) => { const chat = await Chat.create({ visitor_name: data.name, visitor_email: data.email, page_url: data.pageUrl, status: 'waiting', }); socket.join(`chat:${chat.id}`); socket.data.chatId = chat.id; // Notify all operators about a new chat io.to('operators').emit('chat:new', { id: chat.id, visitor_name: chat.visitor_name, page_url: chat.page_url, started_at: chat.created_at, }); socket.emit('chat:started', { chatId: chat.id }); }); // Operator accepts a chat socket.on('chat:accept', async ({ chatId }) => { if (!isOperator) return; const chat = await Chat.findByPk(chatId); if (!chat || chat.status !== 'waiting') return; await chat.update({ operator_id: socket.data.user.id, status: 'active', accepted_at: new Date() }); socket.join(`chat:${chatId}`); // Notify visitor io.to(`chat:${chatId}`).emit('chat:accepted', { operator: { name: socket.data.user.name, avatar: socket.data.user.avatar }, }); }); // Message socket.on('chat:message', async ({ chatId, text }) => { const chat = await Chat.findByPk(chatId); if (!chat) return; const message = await Message.create({ chat_id: chatId, sender_type: isOperator ? 'operator' : 'visitor', sender_id: socket.data.user?.id ?? null, text: sanitize(text), }); io.to(`chat:${chatId}`).emit('chat:message', { id: message.id, text: message.text, sender_type: message.sender_type, sent_at: message.created_at, }); // Update last message time in the chat await chat.update({ last_message_at: new Date() }); }); // Typing... socket.on('chat:typing', ({ chatId }) => { socket.to(`chat:${chatId}`).emit('chat:typing', { sender_type: isOperator ? 'operator' : 'visitor', }); }); // Close chat socket.on('chat:close', async ({ chatId }) => { await Chat.update({ status: 'closed', closed_at: new Date() }, { where: { id: chatId } }); io.to(`chat:${chatId}`).emit('chat:closed'); // Send transcript via email const chat = await Chat.findByPk(chatId, { include: [Message] }); if (chat.visitor_email) { await emailService.sendTranscript(chat); } }); // Operators join a room to receive new chats if (isOperator) { socket.join('operators'); io.to('operators').emit('operator:online', { id: socket.data.user.id }); } socket.on('disconnect', async () => { if (isOperator) { io.to('operators').emit('operator:offline', { id: socket.data.user?.id }); } else if (socket.data.chatId) { // Visitor left io.to(`chat:${socket.data.chatId}`).emit('visitor:left'); } }); }); 

React: Visitor Widget

import { useState, useEffect, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; interface Message { id: string; text: string; sender_type: 'visitor' | 'operator'; sent_at: string; } export function ChatWidget() { const [isOpen, setIsOpen] = useState(false); const [status, setStatus] = useState<'idle' | 'starting' | 'waiting' | 'active' | 'closed'>('idle'); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(''); const [chatId, setChatId] = useState<string | null>(null); const [operator, setOperator] = useState<{ name: string } | null>(null); const socketRef = useRef<Socket | null>(null); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { socketRef.current = io(import.meta.env.VITE_CHAT_SERVER); const socket = socketRef.current; socket.on('chat:started', ({ chatId }) => { setChatId(chatId); setStatus('waiting'); }); socket.on('chat:accepted', ({ operator }) => { setOperator(operator); setStatus('active'); setMessages(prev => [...prev, { id: 'system-accepted', text: `${operator.name} connected to the chat`, sender_type: 'operator', sent_at: new Date().toISOString(), }]); }); socket.on('chat:message', (message) => { setMessages(prev => [...prev, message]); }); socket.on('chat:closed', () => setStatus('closed')); return () => { socket.disconnect(); }; }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const startChat = () => { setStatus('starting'); socketRef.current?.emit('chat:start', { name: 'Visitor', pageUrl: window.location.href, }); }; const sendMessage = () => { if (!input.trim() || !chatId) return; socketRef.current?.emit('chat:message', { chatId, text: input }); setInput(''); }; if (!isOpen) { return ( <button className="chat-trigger" onClick={() => setIsOpen(true)} aria-label="Open chat"> 💬 </button> ); } return ( <div className="chat-widget" role="dialog" aria-label="Support chat"> <header> <h2>{operator ? `Chat with ${operator.name}` : 'Support chat'}</h2> <button onClick={() => setIsOpen(false)} aria-label="Close">×</button> </header> <div className="messages" aria-live="polite"> {status === 'idle' && ( <div className="start-screen"> <p>Hi! How can we help?</p> <button onClick={startChat}>Start chat</button> </div> )} {status === 'waiting' && <p>Looking for an available operator...</p>} {messages.map(msg => ( <div key={msg.id} className={`message message--${msg.sender_type}`}> <p>{msg.text}</p> <time>{new Date(msg.sent_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</time> </div> ))} <div ref={messagesEndRef} /> </div> {status === 'active' && ( <footer> <input value={input} onChange={e => { setInput(e.target.value); socketRef.current?.emit('chat:typing', { chatId }); }} onKeyDown={e => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), sendMessage())} placeholder="Type a message..." /> <button onClick={sendMessage} disabled={!input.trim()}>Send</button> </footer> )} </div> ); } 

Operator Panel

function OperatorDashboard() { const [chats, setChats] = useState<Chat[]>([]); const [activeChat, setActiveChat] = useState<string | null>(null); // Get waiting chat queue socket.on('chat:new', (chat) => { setChats(prev => [chat, ...prev]); // Sound notification new Audio('/notification.mp3').play().catch(() => {}); }); const acceptChat = (chatId: string) => { socket.emit('chat:accept', { chatId }); setActiveChat(chatId); setChats(prev => prev.filter(c => c.id !== chatId)); }; return ( <div className="operator-dashboard"> <aside className="chat-queue"> <h2>Waiting ({chats.length})</h2> {chats.map(chat => ( <div key={chat.id} className="queue-item"> <span>{chat.visitor_name}</span> <span>{chat.page_url}</span> <button onClick={() => acceptChat(chat.id)}>Accept</button> </div> ))} </aside> {activeChat && <ChatWindow chatId={activeChat} socket={socket} />} </div> ); } 

Work Process

  1. Analytics: Determine expected load, number of operators, and integration requirements.
  2. Design: Server architecture, database schema, API contracts.
  3. Implementation: Develop server, widget, and operator panel in parallel.
  4. Testing: Load testing (up to 10K connections), fault tolerance checks.
  5. Deployment: Configure Nginx (WebSocket proxy), Redis adapter for horizontal scaling, monitoring via Prometheus+Grafana.

Integration with CRM via webhooks allows transferring dialog history. If needed, we add a queue (Bull/RabbitMQ) for offline messages and email transcripts.

Handling Offline Messages

If an operator doesn't accept a chat within a given timeout, the message is saved to the database and a transcript is sent to the client's email with a promise to respond during business hours. For high-load businesses, we add escalation: if the queue exceeds a limit, a second operator is added or a Telegram integration is triggered.

What Is Included in the Result

  • API documentation and deployment instructions
  • Ready Docker image for the server
  • Source code of the widget and operator panel
  • Support team training (1 hour online)
  • Free support for 3 months after launch

Our experience includes over 10 projects of similar complexity. We offer a 12-month code warranty. According to Statista, website chat increases conversion by 20-40%. Implementing a support chat using WebSocket is an investment in customer loyalty. Contact us to discuss details and get demo access to the operator panel. Order the development of a live chat for your tasks and get a free consultation.

Implementation Timeline

Task Timeline
Socket.IO server + basic events 2–3 days
Visitor widget (React) 2–3 days
Operator panel 3–4 days
Chat history + email transcripts +1–2 days
Full system with queue and monitoring 8–12 days