nsqio/nsq · warning · ClientErr

ID not in flight

Error message

ID not in flight

What it means

popInFlightMessage is the single lookup path for FIN, REQ and TOUCH: it fetches the MessageID from the channel's inFlightMessages map under inFlightMutex and returns errors.New("ID not in flight") when the ID is absent. The protocol layer wraps it as E_FIN_FAILED / E_REQ_FAILED / E_TOUCH_FAILED ('<CMD> <id> failed ID not in flight'). The ID is absent whenever it was already removed - a duplicate FIN/REQ/TOUCH, a completed timeout requeue, or channel teardown.

Source

Thrown at nsqd/channel.go:562

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")
	}
	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()

View on GitHub (pinned to 85cf10c09c)

Solutions

  1. Treat 'ID not in flight' on FIN/TOUCH as benign idempotency fallout: log it at debug and continue (the message is already requeued and will be redelivered).
  2. Raise --msg-timeout (or the client's per-message timeout) and TOUCH long-running messages before the deadline so ownership is not lost.
  3. Never re-send FIN/REQ for an ID after an error or reconnect without expecting this response; make handlers idempotent per message ID.
  4. During topology changes, stop processing before channels are deleted.
Defensive patterns

Strategy: try-catch

Try / catch

err := conn.FinishMessage(id) // or TouchMessage / RequeueMessage
if err != nil {
	if strings.Contains(err.Error(), "ID not in flight") {
		// already FIN'd, REQ'd, timed out and requeued, or channel state was reset:
		// harmless duplicate ack - ignore, redelivery will happen if needed
		return nil
	}
	return err
}

Prevention

When it happens

Trigger: Sending FIN for a message whose in-flight timeout already fired and requeued it; sending FIN twice for the same ID; sending TOUCH/REQ after an earlier FIN succeeded; sending any of these after DELETE /channel or /topic removed the channel state. Message-level races between client RDH/timeout and server requeue are the classic producer.

Common situations: Slow consumers whose msg_timeout expires while they are still processing: the server requeues the message and the later FIN finds nothing; at-least-once consumers that re-FIN during shutdown; duplicate command emission after client reconnects replay handler state.

Related errors


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