nsqio/nsq · critical · ClientErr

E_TOUCH_FAILED

E_TOUCH_FAILED

Error message

ID already in flight

What it means

When nsqd's message pump delivers a message it registers it as in-flight via StartInFlightTimeout -> pushInFlightMessage, which errors with 'ID already in flight' if the MessageID already exists in the channel's inFlightMessages map. MessageIDs must be globally unique per nsqd; a duplicate means either two messages with colliding IDs (misconfigured shared --node-id across nsqd nodes feeding the same cluster via nsq_to_http-style tooling, or clock/sequence anomalies) or an API-level double registration. On this error the delivering goroutine exits (goto exit in messagePump), dropping that client connection; the same map conflict also makes later TOUCH attempts on the stale ID fail with E_TOUCH_FAILED.

Source

Thrown at nsqd/channel.go:549

func (c *Channel) StartDeferredTimeout(msg *Message, timeout time.Duration) error {
	absTs := time.Now().Add(timeout).UnixNano()
	item := &pqueue.Item{Value: msg, Priority: absTs}
	err := c.pushDeferredMessage(item)
	if err != nil {
		return err
	}
	c.addToDeferredPQ(item)
	return nil
}

// pushInFlightMessage atomically adds a message to the in-flight dictionary
func (c *Channel) pushInFlightMessage(msg *Message) error {
	c.inFlightMutex.Lock()
	_, ok := c.inFlightMessages[msg.ID]
	if ok {
		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")
	}

View on GitHub (pinned to 85cf10c09c)

Solutions

  1. Give every nsqd in the fleet a unique --node-id in [0,1024) (the root cause of real-world ID collisions).
  2. Restart the affected nsqd with the corrected id; in-flight/deferred state is rebuilt and consumers reconnect automatically.
  3. If embedding nsqd in Go, never call StartInFlightMessage/StartInFlightTimeout twice for one message - the pump does it for you on delivery.
  4. Audit for duplicated data directories (same disk dir mounted by two instances).

Example fix

# before (two nodes, same id)
nsqd --node-id=0 ...   # host A
nsqd --node-id=0 ...   # host B
# after
nsqd --node-id=1 ...   # host A
nsqd --node-id=2 ...   # host B
Defensive patterns

Strategy: validation

Validate before calling

// before joining a cluster, assert a unique node id
if nodeID < 0 || nodeID >= 1024 {
	return fmt.Errorf("node id %d out of range [0,1024)", nodeID)
}
if !acquireNodeIDLease(nodeID) { // e.g. ectcd/zookeeper lock or CMDB record
	return fmt.Errorf("node id %d already in use", nodeID)
}

Prevention

When it happens

Trigger: Two nsqd instances running with the same --node-id whose messages converge on one channel (message IDs embed the node id); embedding nsqd and calling channel.StartInFlightTimeout twice for the same msg; a message requeued while a copy of it is still in the in-flight map. Consumer-visible symptom: connection reset during receive or E_TOUCH_FAILED on TOUCH of the ghost entry.

Common situations: Cloning a VM/container including the data dir or a fixed --node-id and running both copies; running test and prod nsqd with id=0 on the same topic; message mirroring setups feeding one channel from two nsqds that share an ID.

Related errors


AI-assisted analysis of nsqio/nsq@85cf10c09c (2026-08-16). Data as JSON: /api/errors/961367cbaaf5e121. Report an issue: GitHub.