netbirdio/netbird · info

read of the policy table timed out

Error message

read of the policy table timed out

What it means

Debug endpoint error from handleClientStatus (proxy/internal/debug/handler.go:425): the requested accountID has no live embedded management client (nbembed.Client) in the provider's registry, so the debug UI/API answers 404 'Client not found: <accountID>'. The provider maps types.AccountID to per-account embedded clients spawned by the proxy.

Source

Thrown at client/internal/debug/nrpt_windows.go:94

	done := make(chan result, 1)
	go func() {
		// the slot is released here rather than by the caller, so a read that
		// outlives the timeout holds it until the provider answers
		defer func() { <-nrptQueryInFlight }()

		text, err := nrptPolicyTableText()
		done <- result{text: text, err: err}
	}()

	select {
	case res := <-done:
		if res.err != nil {
			return nil, res.err
		}
		return parseNRPTPolicyTable(res.text), nil
	case <-time.After(nrptPolicyTimeout):
		return nil, errors.New("read of the policy table timed out")
	}
}

// nrptPolicyTableText calls the policy table method and returns the MOF text of
// its out parameters.
func nrptPolicyTableText() (text string, err error) {
	// COM is per thread, and the collection is short lived, so the thread is
	// pinned for the duration rather than initialized for the process.
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	defer func() {
		// The COM call chain is dynamically typed, so a provider that answers
		// with an unexpected shape must not take the daemon down with it.
		if r := recover(); r != nil {
			err = fmt.Errorf("read NRPT policy table: %v", r)
		}
	}()

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. List live accounts first via the clients overview (ListClientsForDebug-backed page or endpoint) and use an ID that appears there.
  2. If the account should be live, check the proxy logs for why its client is not running (management sync failure, removed domain).
  3. URL-encode the account ID exactly as shown by the list view.

Example fix

# before
curl http://proxy-debug/debug/clients/<stale-or-typo-id>/status
# -> 404 Client not found: <id>

# after: enumerate live accounts, then query one of them
curl http://proxy-debug/debug/clients | jq 'keys'
curl http://proxy-debug/debug/clients/<id-from-list>/status
Defensive patterns

Strategy: validation

Validate before calling

// Call the list endpoint first and only query IDs it returns.
resp, err := http.Get(debugBase + "/clients")
if err != nil {
    return err
}
var live map[string]any
if err := json.NewDecoder(resp.Body).Decode(&live); err != nil {
    return err
}
if _, ok := live[accountID]; !ok {
    return fmt.Errorf("account %s not live; pick from %d known", accountID, len(live))
}

Prevention

When it happens

Trigger: GET /debug/clients/<accountID>/status (or the ?json variant) with an accountID that was never registered, whose client has not started yet, or whose client was removed (account deregistered or listener torn down); also a mistyped or URL-escaped ID.

Common situations: Operator copies an account ID from an older incident after the account disconnected; racing the debug UI against client startup; account removed from management so the proxy dropped its client.

Understand the failure class

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/811664c4abc5af93. Report an issue: GitHub.