nsqio/nsq · error · ClientErr
client does not own message
Error message
client does not own message
What it means
After finding the MessageID in the in-flight map, popInFlightMessage compares msg.clientID with the requester: only the connection that was delivered the message may FIN/REQ/TOUCH it. A mismatch returns errors.New("client does not own message"), surfaced as E_FIN_FAILED/E_REQ_FAILED/E_TOUCH_FAILED. The server does not touch the entry in this case - the message stays owned by the original client.
Source
Thrown at nsqd/channel.go:566
c.inFlightMutex.Unlock()
return errors.New("ID already in flight")
}
c.inFlightMessages[msg.ID] = msg
c.inFlightMutex.Unlock()
return nil
}
// popInFlightMessage atomically removes a message from the in-flight dictionary
func (c *Channel) popInFlightMessage(clientID int64, id MessageID) (*Message, error) {
c.inFlightMutex.Lock()
msg, ok := c.inFlightMessages[id]
if !ok {
c.inFlightMutex.Unlock()
return nil, errors.New("ID not in flight")
}
if msg.clientID != clientID {
c.inFlightMutex.Unlock()
return nil, errors.New("client does not own message")
}
delete(c.inFlightMessages, id)
c.inFlightMutex.Unlock()
return msg, nil
}
func (c *Channel) addToInFlightPQ(msg *Message) {
c.inFlightMutex.Lock()
c.inFlightPQ.Push(msg)
c.inFlightMutex.Unlock()
}
func (c *Channel) removeFromInFlightPQ(msg *Message) {
c.inFlightMutex.Lock()
if msg.index == -1 {
// this item has already been popped off the pqueue
c.inFlightMutex.Unlock()
returnView on GitHub (pinned to 85cf10c09c)
Solutions
- Only FIN/REQ/TOUCH message IDs on the exact connection that received them - pass ownership, not raw IDs, between components.
- On reconnect, discard unfinished in-memory messages; they will timeout and be redelivered to a live subscriber.
- Use one connection per consumer instance and keep per-connection handler state.
- Log these as a symptom of duplicate subscription paths, then fix the topology (e.g. distinct channels per service).
Defensive patterns
Strategy: try-catch
Try / catch
err := conn.FinishMessage(id)
if err != nil {
if strings.Contains(err.Error(), "client does not own message") {
// another connection now owns this delivery (redelivery after timeout):
// do not retry on this connection; drop local state for the ID
handler.Forget(id)
return nil
}
return err
} Prevention
- Keep message ID -> connection affinity: finish every message on the connection that delivered it.
- On reconnect, clear the unfinished-message table instead of replaying FINs on the new connection.
- One consumer connection per process for a given channel; never share ack duty across connections.
When it happens
Trigger: Two connections SUBscribed to the same channel: connection B receives a redelivered copy while connection A still holds the original; whichever connection sends FIN/REQ/TOUCH for an ID currently owned by the other gets this error. Also happens when a client re-subscribes on a NEW connection and then finishes a message ID it received on the old one.
Common situations: Two instances of an app consuming the same channel with duplicated message handling and cross-acks; client libraries that reconnect mid-message but finish using the new connection; test harnesses that share one channel across goroutines with separate connections.
Related errors
AI-assisted analysis of nsqio/nsq@85cf10c09c (2026-08-16).
Data as JSON: /api/errors/7ed504e69f666fd0.
Report an issue: GitHub.