{"record":{"id":"0b77a91ba8d0a757","repo":"OpenNHP/opennhp","slug":"relay-forward-cap-exceeded","errorCode":null,"errorMessage":"relay forward cap exceeded","messagePattern":"relay forward cap exceeded","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"endpoints/server/msghandler.go","lineNumber":973,"sourceCode":"\t\t\t//\n\t\t\t// In practice the OLD <= MaxConnectionsPerRelay invariant\n\t\t\t// means this branch is unreachable for the current\n\t\t\t// constant cap; it becomes reachable if\n\t\t\t// MaxConnectionsPerRelay is ever hot-reloaded to a lower\n\t\t\t// value mid-flight. The fix-up is cheap so do it\n\t\t\t// unconditionally.\n\t\t\tif transferred && curr > 0 {\n\t\t\t\ts.relayConnCount[relayAddrStr]--\n\t\t\t\tif s.relayConnCount[relayAddrStr] == 0 {\n\t\t\t\t\tdelete(s.relayConnCount, relayAddrStr)\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.relayConnCountMutex.Unlock()\n\t\t\ts.remoteConnectionMapMutex.Unlock()\n\t\t\ts.device.ReleasePoolPacket(innerPkt)\n\t\t\tlog.Critical(\"server-relay[HandleRelayForward] relay %s exceeded MaxConnectionsPerRelay (%d), dropping forward\",\n\t\t\t\trelayAddrStr, MaxConnectionsPerRelay)\n\t\t\treturn fmt.Errorf(\"relay forward cap exceeded\")\n\t\t}\n\t\tif !transferred {\n\t\t\ts.relayConnCount[relayAddrStr]++\n\t\t}\n\t\ts.relayConnCountMutex.Unlock()\n\n\t\tconn = &UdpConn{mapKey: connKey}\n\t\tconn.ConnData = &core.ConnectionData{\n\t\t\tInitTime:          recvTime,\n\t\t\tLastLocalRecvTime: recvTime,\n\t\t\tDevice:            s.device,\n\t\t\tLocalAddr:         s.listenAddr,\n\t\t\tRemoteAddr:        relayAddr,\n\t\t\tRealRemoteAddr:    realAddr,\n\t\t\t// CookieStore omitted: see udpserver.go for rationale.\n\t\t\tRemoteTransactionMap: make(map[uint64]*core.RemoteTransaction),\n\t\t\tTimeoutMs:            DefaultAgentConnectionTimeoutMs,\n\t\t\tSendQueue:            make(chan *core.Packet, PacketQueueSizePerConnection),","sourceCodeStart":955,"sourceCodeEnd":991,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/msghandler.go#L955-L991","documentation":"Each relay address is limited to MaxConnectionsPerRelay forwarded client connections. When a relay exceeds that cap the forward is dropped and this error returned, preventing one compromised or busy relay from monopolizing the server connection table.","triggerScenarios":"relayConnCount[relayAddrStr] would exceed MaxConnectionsPerRelay after this forward and no stale slot could be transferred to the new client.","commonSituations":"Many clients NATed behind a single relay IP; relay leaking forwards; per-relay cap set too low relative to relay subscriber count.","solutions":["Raise MaxConnectionsPerRelay for high-capacity relays","Distribute clients across multiple relay addresses","Check the relay for leaked/stale forwards that never release slots","Clean up relayConnCount entries when relay connections close"],"exampleFix":"// before\nconst MaxConnectionsPerRelay = 64\n// after\nconst MaxConnectionsPerRelay = 512 // sized per relay load","handlingStrategy":"fallback","validationCode":"if relayConnCount[relayAddr] >= maxConnectionsPerRelay {\n    return fmt.Errorf(\"relay %s at per-relay cap\", relayAddr)\n}","typeGuard":null,"tryCatchPattern":"if err := forwardViaRelay(pkt); err != nil {\n    if strings.Contains(err.Error(), \"relay forward cap exceeded\") {\n        return tryOtherRelay(pkt) // fail over to another relay\n    }\n    return err\n}","preventionTips":["Raise MaxConnectionsPerRelay for high-volume relays","Distribute clients over multiple relays","Audit relay slot release on disconnect"],"tags":["relay","capacity","rate-limit","connection-limit"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}