gorilla/websocket · critical
concurrent write to websocket connection
Error message
concurrent write to websocket connection
What it means
This panic fires when a write is started on a connection whose isWriting flag is already true, meaning two goroutines attempted to write concurrently. gorilla/websocket only supports one concurrent writer; this best-effort detection protects the connection from interleaved, corrupted frames.
Source
Thrown at conn.go:626
c.writeBuf[framePos] = b0
c.writeBuf[framePos+1] = b1 | byte(length)
}
if !c.isServer {
key := newMaskKey()
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
if len(extra) > 0 {
return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode")))
}
}
// Write the buffers to the connection with best-effort detection of
// concurrent writes. See the concurrency section in the package
// documentation for more info.
if c.isWriting {
panic("concurrent write to websocket connection")
}
c.isWriting = true
err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra)
if !c.isWriting {
panic("concurrent write to websocket connection")
}
c.isWriting = false
if err != nil {
return w.endMessage(err)
}
if final {
_ = w.endMessage(errWriteClosed)
return nil
}View on GitHub (pinned to e064f32e36)
Solutions
- Serialize all writes through a single goroutine with a channel of outbound messages
- Guard writes with a sync.Mutex held around every WriteMessage/WriteControl call on that Conn
- Move keepalive pings into the same writer goroutine instead of a separate timer goroutine
- Recover from the panic per-connection and close/reconnect, since state may be corrupted
Example fix
// before
go func(){ conn.WriteMessage(TextMessage, msg) }()
go func(){ conn.WriteMessage(TextMessage, other) }()
// after
var mu sync.Mutex
func safeWrite(conn *websocket.Conn, data []byte) {
mu.Lock()
defer mu.Unlock()
conn.WriteMessage(websocket.TextMessage, data)
} Defensive patterns
Strategy: try-catch
Try / catch
func safeWrite(conn *websocket.Conn, mt int, data []byte) (err error) {
defer func() { recover() }() // guard against concurrent-write panic
mu.Lock()
defer mu.Unlock()
return conn.WriteMessage(mt, data)
} Prevention
- One writer goroutine per connection, fed by a channel
- Or a sync.Mutex around every write call
- Move keepalive pings into the serialized write path
- Review fan-out code for multiple write call sites
When it happens
Trigger: Two goroutines simultaneously calling WriteMessage, WriteControl paths via prepareWrite/flushFrame, or NextWriter-based writes on the same Conn; the second write observes isWriting==true at conn.go:626.
Common situations: Broadcasting messages to a connection from multiple goroutines (e.g. a hub fan-out plus a direct write), ping keepalive goroutines writing while the main handler writes, or sending from websocket event callbacks and background timers at the same time.
Related errors
- websocket: close sent
- websocket: write closed
- repeated read on failed websocket connection
- websocket: bad handshake
- websocket: invalid compression negotiation
AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31).
Data as JSON: /api/errors/97ab3396c8a80baa.
Report an issue: GitHub.