OpenNHP/opennhp · error
server has no usable instance
Error message
server has no usable instance
What it means
handleRelay responds with HTTP 503 'server has no usable instance' when sticky sessions are enabled, multiple instances exist, and the picker cannot obtain an instance for the client's hashed address key (PickByKey returned !ok). The relay cannot forward the packet because no backend instance is available for this client's sticky slot.
Solutions
- Verify at least one backend NHP server instance is running and reachable from the relay
- Check the relay's server configuration table for correct addresses and reload it
- Inspect instance health/remove logic logs to see why the picker has no entry for the key
- Temporarily disable StickyInstance (single-instance or LB mode) to confirm the backend is the problem
Example fix
// before: sticky enabled with dead backends
sticky = true
// after: ensure healthy instance exists, or fall back
if !pickerHealthy(inst) { inst = cr.pickInstance() } Defensive patterns
Strategy: retry
Validate before calling
// health-check backends before relying on sticky routing
for _, s := range backends {
if !tcpReachable(s.addr) { log.Printf("backend down: %s", s.addr) }
} Try / catch
if resp.StatusCode == http.StatusServiceUnavailable {
return retryWithBackoff(func() error { return knock(relayURL, packet) })
} Prevention
- Monitor backend NHP server health from the relay
- Keep the relay peer table populated and reload on change
- Alert on 503 rates from the relay endpoint
When it happens
Trigger: POSTing to a relay whose sticky option is on with >1 configured instances while the sticky picker fails to resolve a key — e.g. no healthy instances registered, picker initialization failure, or all instances for that key removed.
Common situations: Backend NHP servers down so instances were pruned from the picker; misconfigured server table leaving zero healthy instances; race during shutdown where instances were drained before requests stopped arriving.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- relay: failed to parse config
- relay: privateKeyBase64 must be set in config
- relay: no upstream configured; add at least one [[Servers]]…
- relay: server # missing publicKeyBase64
- relay: server # publicKeyBase64 invalid
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/abd02c4ccc82eb95.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/relay/relay.go:1003
realAddr, err := realClientAddr(r)
if err != nil {
log.Error("[Relay] %v", err)
http.Error(w, "relay misconfigured: missing X-Real-IP header from local reverse proxy", http.StatusBadGateway)
return
}
realAddrKey := realAddr.String()
// Pick a target instance. When StickyInstance is enabled,
// hash the real client IP so the same client always reaches the same
// instance — required for stateful flows like OTP→REG where per-
// instance local state (SQLite) must be consistent across requests.
// When disabled (default), each request is load-balanced independently.
var inst *serverInstance
if cr.sticky && len(cr.instances) > 1 {
var ok bool
inst, ok = cr.picker.PickByKey(realAddrKey)
if !ok {
http.Error(w, "server has no usable instance", http.StatusServiceUnavailable)
return
}
} else {
inst = cr.pickInstance()
}
if inst == nil {
http.Error(w, "server has no usable instance", http.StatusServiceUnavailable)
return
}
// Bound concurrent forwards (and thus pendingRequests size) per
// instance. Non-blocking acquire: if the instance is saturated, shed
// the request with 503 rather than queueing — the alternative is the
// pending map growing unbounded when an adversary opens faster than
// the 5s handler timeout drains. Released on handler return.
select {
case inst.inFlight <- struct{}{}:
defer func() { <-inst.inFlight }()View on GitHub (pinned to 6e04ca5ff0)