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

  1. Increase MaxConcurrentConnection to match expected peak load
  2. Ensure stale connections expire and release slots (check timeouts/cleanup)
  3. Investigate sources of connection churn (relay retries, client floods)
  4. 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

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


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)