{"record":{"id":"7ed504e69f666fd0","repo":"nsqio/nsq","slug":"client-does-not-own-message","errorCode":null,"errorMessage":"client does not own message","messagePattern":"client does not own message","errorType":"exception","errorClass":"ClientErr","httpStatus":null,"severity":"error","filePath":"nsqd/channel.go","lineNumber":566,"sourceCode":"\t\tc.inFlightMutex.Unlock()\n\t\treturn errors.New(\"ID already in flight\")\n\t}\n\tc.inFlightMessages[msg.ID] = msg\n\tc.inFlightMutex.Unlock()\n\treturn nil\n}\n\n// popInFlightMessage atomically removes a message from the in-flight dictionary\nfunc (c *Channel) popInFlightMessage(clientID int64, id MessageID) (*Message, error) {\n\tc.inFlightMutex.Lock()\n\tmsg, ok := c.inFlightMessages[id]\n\tif !ok {\n\t\tc.inFlightMutex.Unlock()\n\t\treturn nil, errors.New(\"ID not in flight\")\n\t}\n\tif msg.clientID != clientID {\n\t\tc.inFlightMutex.Unlock()\n\t\treturn nil, errors.New(\"client does not own message\")\n\t}\n\tdelete(c.inFlightMessages, id)\n\tc.inFlightMutex.Unlock()\n\treturn msg, nil\n}\n\nfunc (c *Channel) addToInFlightPQ(msg *Message) {\n\tc.inFlightMutex.Lock()\n\tc.inFlightPQ.Push(msg)\n\tc.inFlightMutex.Unlock()\n}\n\nfunc (c *Channel) removeFromInFlightPQ(msg *Message) {\n\tc.inFlightMutex.Lock()\n\tif msg.index == -1 {\n\t\t// this item has already been popped off the pqueue\n\t\tc.inFlightMutex.Unlock()\n\t\treturn","sourceCodeStart":548,"sourceCodeEnd":584,"githubUrl":"https://github.com/nsqio/nsq/blob/85cf10c09c6c3c86160d6f0eb156f62d0efc1648/nsqd/channel.go#L548-L584","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Only FIN/REQ/TOUCH message IDs on the exact connection that received them - pass ownership, not raw IDs, between components.","On reconnect, discard unfinished in-memory messages; they will timeout and be redelivered to a live subscriber.","Use one connection per consumer instance and keep per-connection handler state.","Log these as a symptom of duplicate subscription paths, then fix the topology (e.g. distinct channels per service)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := conn.FinishMessage(id)\nif err != nil {\n\tif strings.Contains(err.Error(), \"client does not own message\") {\n\t\t// another connection now owns this delivery (redelivery after timeout):\n\t\t// do not retry on this connection; drop local state for the ID\n\t\thandler.Forget(id)\n\t\treturn nil\n\t}\n\treturn err\n}","preventionTips":["Keep message ID -> connection affinity: finish every message on the connection that delivered it.","On reconnect, clear the unfinished-message table instead of replaying FINs on the new connection.","One consumer connection per process for a given channel; never share ack duty across connections."],"tags":["nsq","nsqd","protocol","concurrency","ownership"],"backgroundTag":null,"analyzedSha":"85cf10c09c6c3c86160d6f0eb156f62d0efc1648","analyzedAt":"2026-08-16T00:53:05.009Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}