OpenNHP/opennhp · critical
server connection table full
Error message
server connection table full
What it means
Before accepting a relayed forward the server checks total concurrent connections against MaxConcurrentConnection. When the table is full the forward is dropped, the pooled packet released, and this error returned. Exceeding OverloadConnectionThreshold additionally flips the device into overload mode.
Solutions
- Increase MaxConcurrentConnection to match expected peak load
- Ensure stale connections expire and release slots (check timeouts/cleanup)
- Investigate sources of connection churn (relay retries, client floods)
- Enable overload shedding upstream so excess traffic is refused earlier
Example fix
// before const MaxConcurrentConnection = 1024 // after const MaxConcurrentConnection = 8192 // sized from peak metrics
Defensive patterns
Strategy: fallback
Validate before calling
if totalConnections >= maxConcurrentConnection {
return fmt.Errorf("server at capacity; shedding load")
} Try / catch
if err := forwardViaRelay(pkt); err != nil {
if strings.Contains(err.Error(), "connection table full") {
// shed, queue, or retry against another server instance
return ErrServerAtCapacity
}
return err
} Prevention
- Size MaxConcurrentConnection from peak metrics
- Ensure connection timeouts reap stale slots
- Deploy load balancing across multiple servers
When it happens
Trigger: Total tracked connections >= MaxConcurrentConnection at the time HandleRelayForward tries to register the forwarded client connection.
Common situations: Traffic spikes or DDoS exhausting the connection table; leaked connections never timing out; MaxConcurrentConnection set too low for production load.
Related errors
- relay forward cap exceeded
- rkn rate limit exceeded
- server instance busy
- relay overloaded
- relay: failed to parse config
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/c10978b812fd4838.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/msghandler.go:924
s.remoteConnectionMapMutex.Unlock()
// If we got here via the stale-replace path we already
// deleted the OLD entry and marked it replaced, so the
// OLD conn's teardown will skip its dec (stillPresent=false
// AND replaced=true). Refusing to take over the slot here
// without compensating would leak it permanently — the
// per-relay counter would stay elevated with no live owner,
// eventually capping the relay below MaxConnectionsPerRelay.
// Mirror the per-relay branch's fix-up: reclaim the dec
// ourselves. (decRelayConnCount takes relayConnCountMutex on
// its own; do it after releasing the map mutex to preserve
// the map→counter lock order used elsewhere.)
if transferred {
s.decRelayConnCount(relayAddrStr)
}
s.device.ReleasePoolPacket(innerPkt)
log.Critical("server-relay[HandleRelayForward] reached MaxConcurrentConnection (%d), dropping forward from relay %s",
MaxConcurrentConnection, relayAddrStr)
return fmt.Errorf("server connection table full")
}
if total > OverloadConnectionThreshold {
s.device.SetOverload(true)
}
// Per-relay cap: if we're transferring a slot from a stale
// conn the counter already accounts for it, so just check the
// cap; otherwise this is a fresh slot and we must inc.
s.relayConnCountMutex.Lock()
curr := s.relayConnCount[relayAddrStr]
// The cap compares against curr (slot transfer) or curr+1
// (genuinely new slot). Use the post-action value either way
// so the check is uniform.
post := curr
if !transferred {
post = curr + 1
}
if post > MaxConnectionsPerRelay {View on GitHub (pinned to 6e04ca5ff0)