How do you implement WebSockets in Go? How do you handle concurrent writes, ping/pong keepalives and backpressure?
Question 574HardGo 1.22 to 1.25
The standard library has no WebSocket server. Use github.com/gorilla/websocket or github.com/coder/websocket (formerly nhooyr, context-based and safe for concurrent writes).
With gorilla, each connection allows one concurrent reader and one concurrent writer. Concurrent WriteMessage calls corrupt frames or panic. Close and WriteControl are the exceptions and are safe to call concurrently. The standard design is two goroutines per connection:
- A read pump that owns reads and handles pongs.
- A write pump that owns writes and sends pings.
Everyone else talks to the connection through a bounded channel. Backpressure means that when a client is too slow and its buffer fills, you drop the message or disconnect the client. You must never block the broadcaster.
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = pongWait * 9 / 10
)
type client struct {
conn *websocket.Conn
send chan []byte // bounded, for example make(chan []byte, 256)
}
func (c *client) readPump(onMsg func([]byte)) {
defer c.conn.Close()
c.conn.SetReadLimit(64 << 10)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
return c.conn.SetReadDeadline(time.Now().Add(pongWait))
})
for {
_, msg, err := c.conn.ReadMessage()
if err != nil {
return // timeout or close; the hub closes c.send on unregister
}
onMsg(msg)
}
}
func (c *client) writePump() {
t := time.NewTicker(pingPeriod)
defer func() { t.Stop(); c.conn.Close() }()
for {
select {
case msg, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, nil)
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-t.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// broadcaster: never block on a slow client
func (c *client) trySend(msg []byte) bool {
select {
case c.send <- msg:
return true
default:
return false // buffer full: unregister and close this client
}
}
Gotchas:
- Set a read limit, or a client can send a huge frame and exhaust memory.
- Check the
Originheader withUpgrader.CheckOriginto prevent cross-site WebSocket hijacking. - Deadlines are how you detect dead TCP peers. Without them, goroutines leak.
http.Server.Shutdowndoes not close hijacked connections. Track them and close them yourself.- Only one goroutine may close the
sendchannel. Let the hub own it.
More on Observability, Debugging & Production Operations
- Q572How do you manage configuration in Go (env vars, files, flags)? How do you validate it and reload it safely at run time?
- Q573How do you configure TLS correctly in Go (crypto/tls MinVersion, certificate reloading, mTLS)?
- Q575What do runtime/debug.SetMaxStack, SetMaxThreads, SetGCPercent and SetMemoryLimit do, and when would you use them?
- Q576How do you monitor Go runtime health in production (runtime/metrics, goroutine counts, GC pause, heap), and what alerts would you set?
- Q577How do you detect and fix high latency caused by lock contention, GC pressure, or connection-pool exhaustion using production telemetry?