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
- 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).
- Raise --msg-timeout (or the client's per-message timeout) and TOUCH long-running messages before the deadline so ownership is not lost.
- Never re-send FIN/REQ for an ID after an error or reconnect without expecting this response; make handlers idempotent per message ID.
- 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
- Deduplicate FIN/REQ per message ID inside your handler wrapper (sync.Map of in-flight IDs) so commands are never sent twice.
- TOUCH long-running messages before msg-timeout expires so ownership is not silently lost to the timeout requeue.
- Log 'ID not in flight' at INFO with metrics - a rising rate means timeouts are too tight, not that data is lost.
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.