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
- Give every nsqd in the fleet a unique --node-id in [0,1024) (the root cause of real-world ID collisions).
- Restart the affected nsqd with the corrected id; in-flight/deferred state is rebuilt and consumers reconnect automatically.
- If embedding nsqd in Go, never call StartInFlightMessage/StartInFlightTimeout twice for one message - the pump does it for you on delivery.
- 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
- Assign --node-id from a central registry with ownership records; never bake one id into a golden image.
- Monitor for it: repeated connection resets during delivery plus E_TOUCH_FAILED 'ID already in flight' patterns indicate id collisions.
- When embedding nsqd, leave in-flight bookkeeping to the message pump; do not call StartInFlightTimeout manually.
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.