A reliable Go chat server gives each WebSocket connection one reader and one writer, while a Hub owns the client map and broadcasts messages through bounded per-client queues. This design keeps connection I/O separate from shared membership state and gives the server a clear way to handle slow clients, disconnects, and liveness checks.
Choose the ownership model before writing handlers
For a single-process chat app, use a Hub goroutine to own the set of connected clients. Each Client represents one WebSocket connection and holds a pointer to the Hub plus a buffered channel for messages waiting to be sent to that connection.
The Hub receives registration, removal, and broadcast events through channels. Because its event loop is the only code that changes the client map, handlers and connection pumps do not need to mutate that map directly. This follows the Go guidance in Effective Go: “Do not communicate by sharing memory; instead, share memory by communicating.”
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte),
}
}
Start hub.run() once when the server starts. The following loop owns all map changes and enforces the slow-client policy: if a client’s outbound queue is full, remove it and close its queue rather than blocking every other recipient.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →#1 Best Overall
func (h *Hub) run() {
for {
select {
case c := <-h.register:
h.clients[c] = true
case c := <-h.unregister:
if h.clients[c] {
delete(h.clients, c)
close(c.send)
}
case message := <-h.broadcast:
for c := range h.clients {
select {
case c.send <- message:
default:
delete(h.clients, c)
close(c.send)
}
}
}
}
}
This example assumes one Hub event loop stays alive for the lifetime of the server. Keep channel closure in that owner: closing a client’s send channel in multiple places can panic, and allowing a connection pump to change the map would defeat the ownership model. A real application should also define what happens to messages accepted just before shutdown.
Upgrade requests only after applying your access rules
Before upgrading an HTTP request, perform any required authentication and authorization. Then use a websocket.Upgrader with an explicit origin allowlist for browser clients. An origin check is not authentication: it limits which browser origins can initiate a connection, but it does not identify or authorize the user.
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://chat.example.com"
},
}
Replace the example origin with the origins your deployment actually serves. Do not use a permissive development check in production. Gorilla WebSocket’s package documentation describes the Upgrader pattern and warns that the deprecated package-level Upgrade function does not perform origin checking.
Once upgrade succeeds, create a client with a bounded queue, register it, start its writer, and keep the handler goroutine in the reader:
const outboundQueueSize = 64
func serveChat(hub *Hub, w http.ResponseWriter, r *http.Request) {
// Authenticate and authorize r before upgrading in a real application.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
c := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, outboundQueueSize),
}
hub.register <- c
go c.writePump()
c.readPump()
}
The queue size above is an example setting, not a capacity recommendation. Choose it from observed message sizes, fan-out rates, memory limits, and the experience you want for a client that cannot keep up.
Give each WebSocket exactly one reader and one writer
Gorilla WebSocket documents that “Connections support one concurrent reader and one concurrent writer.” Keep all read operations for a connection in readPump and all data writes in writePump. The pumps may run concurrently with each other, but do not add another goroutine that writes chat messages directly to the same connection.
Reader: validate input and hand it to the Hub
The reader owns ReadMessage, the read deadline, and the pong handler. Set a message-size limit before reading. The example below accepts text frames and forwards their bytes; production code should also validate the message format, enforce application-level rules, and decide whether empty or malformed messages are rejected.
const (
maxMessageSize = 4096
pongWait = 60 * time.Second
)
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
return c.conn.SetReadDeadline(time.Now().Add(pongWait))
})
for {
messageType, message, err := c.conn.ReadMessage()
if err != nil {
return
}
if messageType != websocket.TextMessage {
continue
}
c.hub.broadcast <- message
}
}
The constants illustrate a lifecycle pattern, not universal timeout or message-size values. Tune limits for the application and align client behavior with them. If binary messages are part of the protocol, handle them explicitly rather than silently treating them as text.
Recommended Free Tools
Writer: drain the queue, send pings, and set deadlines
The writer owns data writes, ping writes, and write deadlines. When the Hub closes send, the writer attempts a close frame and exits. A periodic ping prompts the peer to respond; the reader’s pong handler refreshes the read deadline.
Rank #4
const (
writeWait = 10 * time.Second
pingPeriod = 54 * time.Second
)
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(
websocket.CloseMessage,
[]byte{},
)
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(
websocket.PingMessage,
nil,
); err != nil {
return
}
}
}
}
Here pingPeriod is shorter than pongWait, so a responsive peer has time to answer before the read deadline expires. If a read or write fails, the pump exits and closes the connection. A read failure triggers unregistration; if the writer fails first, closing the connection causes the reader to observe its read error and unregister the client.
Make backpressure and delivery semantics explicit
WebSocket gives the browser a bidirectional connection, but the stable browser WebSocket API does not provide backpressure. If messages arrive faster than client code can process them, queued data and memory use can grow. Bound server-side queues and message sizes, then choose a policy for a full queue deliberately.
| Policy or design | Behavior | Trade-off |
|---|---|---|
| Disconnect a slow client | Stop queueing to that client and close its connection. | Preserves bounded memory and makes the failure visible, but the client must reconnect and may miss messages. |
| Drop messages | Keep the connection but discard messages when its queue is full. | Can preserve availability, but loses chat messages unless the app communicates the loss or supports recovery. |
| Hub event loop | One goroutine owns membership and serializes register, unregister, and broadcast events. | Centralizes state and is straightforward to reason about in one process. |
| Shared map with locks | Multiple components access membership while holding a lock. | Can suit systems where several independent subsystems need direct access, but requires careful lock discipline. |
| One process | One Hub fans messages out to clients connected to that process. | Does not broadcast to clients connected to other server instances. |
| Multiple instances | Instances need an external pub/sub or message broker for cross-instance fan-out. | Presence, ordering, and duplicate delivery need explicit design; no particular broker or delivery guarantee follows from the Hub pattern. |
| Text frames | Carry readable text, commonly a JSON envelope for chat. | Convenient for application messages, but the schema still needs versioning and validation. |
| Binary frames | Carry bytes under an application-defined protocol. | May avoid some text encoding work, but clients need a documented schema. Gorilla exposes distinct text and binary message types. |
The Gorilla example also coalesces queued messages into a single WebSocket message to reduce system calls. That is an optimization with a protocol consequence: the client must know how to split or decode the combined payload. Do not coalesce independent JSON objects without defining an unambiguous framing format.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Best Value
Build a browser client that treats chat text as untrusted
Use wss:// when the page is served over TLS; for local development, the connection may use ws://. Attach handlers for connection lifecycle events, send user input only when the socket is open, and render incoming content as text rather than inserting it as HTML.
const socket = new WebSocket("wss://chat.example.com/ws");
const log = document.querySelector("#messages");
const form = document.querySelector("#chat-form");
const input = document.querySelector("#message");
socket.onopen = () => {
// Enable the send control here.
};
socket.onmessage = (event) => {
const line = document.createElement("div");
line.textContent = event.data;
log.appendChild(line);
};
socket.onerror = () => {
// Show a useful connection error without exposing sensitive details.
};
socket.onclose = () => {
// Update the UI and offer a deliberate reconnect action if appropriate.
};
form.addEventListener("submit", (event) => {
event.preventDefault();
if (socket.readyState !== WebSocket.OPEN) return;
socket.send(input.value);
input.value = "";
});
For a production protocol, send and receive a versioned JSON envelope instead of relying on unstructured text, and validate both directions. If rendering formatted or rich text is a requirement, sanitize it with a suitable HTML-sanitization approach rather than assigning untrusted message content to innerHTML.
Test failure paths, not only the happy path
Run go test and go test -race in the project environment; the race detector is useful for finding unsynchronized access, but neither command substitutes for exercising real connection lifecycles. Test at least these cases:
- Connect two browser clients and verify that a message from one reaches the other.
- Close a tab abruptly and interrupt the network; confirm the client is unregistered and its queue is closed once.
- Send malformed and oversized messages, and verify the server rejects or closes the connection according to the protocol.
- Attempt a connection from a disallowed browser origin and confirm the upgrade is rejected.
- Simulate a slow reader until its outbound queue fills; verify the configured drop or disconnect behavior and that other clients still receive messages.
- Check close frames, ping/pong timeouts, write failures, and that no goroutine stays blocked after a client leaves.
Measure fan-out latency and memory under representative message sizes and client counts before choosing queue capacities. There is no universal throughput or connection-capacity figure for a complete deployment; results depend on the implementation, workload, and operating environment.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Know when WebSockets are not the right fit
WebSockets are a broadly supported option for bidirectional browser communication. MDN describes WebSocketStream as non-standard and WebTransport as a more complex alternative with additional delivery features. The choice depends on client support requirements and transport semantics; changing transports does not remove the need to design authorization, message limits, delivery behavior, and lifecycle cleanup.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

