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

  1. Verify at least one backend NHP server instance is running and reachable from the relay
  2. Check the relay's server configuration table for correct addresses and reload it
  3. Inspect instance health/remove logic logs to see why the picker has no entry for the key
  4. 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

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


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)