OpenNHP/opennhp · critical
relay forward cap exceeded
Error message
relay forward cap exceeded
What it means
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.
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
Example fix
// before const MaxConnectionsPerRelay = 64 // after const MaxConnectionsPerRelay = 512 // sized per relay load
Defensive patterns
Strategy: fallback
Validate before calling
if relayConnCount[relayAddr] >= maxConnectionsPerRelay {
return fmt.Errorf("relay %s at per-relay cap", relayAddr)
} Try / catch
if err := forwardViaRelay(pkt); err != nil {
if strings.Contains(err.Error(), "relay forward cap exceeded") {
return tryOtherRelay(pkt) // fail over to another relay
}
return err
} Prevention
- Raise MaxConnectionsPerRelay for high-volume relays
- Distribute clients over multiple relays
- Audit relay slot release on disconnect
When it happens
Trigger: relayConnCount[relayAddrStr] would exceed MaxConnectionsPerRelay after this forward and no stale slot could be transferred to the new client.
Common situations: Many clients NATed behind a single relay IP; relay leaking forwards; per-relay cap set too low relative to relay subscriber count.
Related errors
- server connection table full
- rkn rate limit exceeded
- relay: failed to parse config
- relay: privateKeyBase64 must be set in config
- relay: no upstream configured; add at least one [[Servers]]…
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/0b77a91ba8d0a757.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/msghandler.go:973
//
// In practice the OLD <= MaxConnectionsPerRelay invariant
// means this branch is unreachable for the current
// constant cap; it becomes reachable if
// MaxConnectionsPerRelay is ever hot-reloaded to a lower
// value mid-flight. The fix-up is cheap so do it
// unconditionally.
if transferred && curr > 0 {
s.relayConnCount[relayAddrStr]--
if s.relayConnCount[relayAddrStr] == 0 {
delete(s.relayConnCount, relayAddrStr)
}
}
s.relayConnCountMutex.Unlock()
s.remoteConnectionMapMutex.Unlock()
s.device.ReleasePoolPacket(innerPkt)
log.Critical("server-relay[HandleRelayForward] relay %s exceeded MaxConnectionsPerRelay (%d), dropping forward",
relayAddrStr, MaxConnectionsPerRelay)
return fmt.Errorf("relay forward cap exceeded")
}
if !transferred {
s.relayConnCount[relayAddrStr]++
}
s.relayConnCountMutex.Unlock()
conn = &UdpConn{mapKey: connKey}
conn.ConnData = &core.ConnectionData{
InitTime: recvTime,
LastLocalRecvTime: recvTime,
Device: s.device,
LocalAddr: s.listenAddr,
RemoteAddr: relayAddr,
RealRemoteAddr: realAddr,
// CookieStore omitted: see udpserver.go for rationale.
RemoteTransactionMap: make(map[uint64]*core.RemoteTransaction),
TimeoutMs: DefaultAgentConnectionTimeoutMs,
SendQueue: make(chan *core.Packet, PacketQueueSizePerConnection),View on GitHub (pinned to 6e04ca5ff0)