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()
		return

View on GitHub (pinned to 85cf10c09c)

Solutions

  1. Only FIN/REQ/TOUCH message IDs on the exact connection that received them - pass ownership, not raw IDs, between components.
  2. On reconnect, discard unfinished in-memory messages; they will timeout and be redelivered to a live subscriber.
  3. Use one connection per consumer instance and keep per-connection handler state.
  4. 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

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.