Xray API Node Switching Scripts: An Advanced Automation Guide

Build a practical Xray automation workflow that measures node health, selects the best outbound, and recovers from failures without manual edits. The guide combines API concepts with ready-to-adapt JSON and scripting patterns for developers and network operators.

Node switching becomes difficult when a client has more than a few subscription entries. Manually selecting a node only reacts to a visible failure; it does not measure latency, packet loss, handshake errors, or recovery time. An automation workflow should instead define what “healthy” means, test candidates without disturbing active traffic, select an eligible outbound, and avoid switching repeatedly when the network is unstable.

Xray exposes a local gRPC API that can be used by a controller, shell script, Python service, or scheduled task. The API is not a complete node-management platform by itself: it reports runtime statistics and supports selected runtime operations, while subscription parsing, active probing, scoring, state storage, and safe fallback remain the responsibility of the automation layer. A reliable design therefore separates measurement from switching and treats configuration changes as controlled deployments rather than arbitrary edits to a running process.

Quick overview

This guide shows how to build an Xray node-switching controller around the local API. It covers API exposure, health checks, scoring, generated configuration, safe switching, cooldowns, logging, and recovery. The examples use Xray-core 25.x-style JSON concepts, a local API on port 10085, and a controller that keeps the active outbound separate from the node pool.

Design the automation boundary before writing code

A node switcher normally contains five independent parts: a node inventory, a probe mechanism, a scoring function, an actuator, and a state store. The inventory describes each outbound and its stable identifier. The probe mechanism measures whether that outbound can actually reach a controlled destination. The scoring function converts measurements into a comparable value. The actuator changes the active route. The state store remembers the current node, recent failures, cooldown timers, and the last successful probe.

Load node inventory Run isolated probes Calculate health score Select eligible outbound Apply route change Verify active traffic

Do not equate an API response with end-to-end availability. Xray may be running normally while a remote node is unreachable. Conversely, a node may answer a TCP handshake but fail when the application requests a real HTTPS destination. Use at least two levels of checks: a local process check and an outbound path check. The local check confirms that the API endpoint and core are alive. The outbound check confirms that a particular node can carry traffic.

  • Process health: the Xray process is running and the API accepts a request on 127.0.0.1:10085.
  • Transport health: the node can complete its configured TCP, TLS, WebSocket, REALITY, or other transport handshake.
  • Application health: a controlled HTTPS request completes through the candidate outbound.
  • Stability health: repeated probes do not show timeouts, resets, or excessive latency.
10085
Local Xray API port in this example
3
Minimum consecutive samples before promotion
30 s
Suggested switch cooldown
2 min
Suggested quarantine period after failure

Keep the API bound to a loopback address unless a strongly controlled management network is required. An Xray API is an administrative interface, not a public health-check endpoint. Binding it to 0.0.0.0 without firewall restrictions can expose statistics and runtime controls to other hosts. A controller should also use a dedicated API tag, a separate API inbound, and a predictable local port so that the management path does not depend on the node currently being tested.

{
  "api": {
    "tag": "api"
  },
  "inbounds": [
    {
      "tag": "api-in",
      "listen": "127.0.0.1",
      "port": 10085,
      "protocol": "dokodemo-door",
      "settings": {
        "address": "127.0.0.1"
      }
    }
  ],
  "routing": {
    "rules": [
      {
        "type": "field",
        "inboundTag": ["api-in"],
        "outboundTag": "api"
      }
    ]
  }
}

The exact complete configuration still needs a valid node outbound, logging section, DNS section, and any transport objects required by the selected protocol. The API fragment above illustrates the control-plane boundary; it is not intended to replace a complete production configuration. Test the resulting file with the Xray configuration check command before restarting or reloading a core.

Expose and query the Xray API safely

Xray’s API uses gRPC services. The statistics service can report counters for named objects when the corresponding policy enables statistics. This is useful for observing traffic volume, but byte counters alone cannot tell you that a node is good. A node that is transferring a small amount of data may simply be idle. Treat counters as supporting evidence and combine them with active probes.

For local administration, the API address is commonly written as 127.0.0.1:10085. A command-line query can confirm that the endpoint is responding:

xray api statsquery --server=127.0.0.1:10085

When using named outbounds, keep names stable. Do not use display names copied from a subscription as the only identifier because a provider may rename a node on the next update. A safer inventory stores an internal key, a human-readable label, and the outbound tag separately:

{
  "nodes": [
    {
      "id": "node-sg-01",
      "label": "Singapore 01",
      "outboundTag": "pool-sg-01",
      "probeUrl": "https://connectivity.example.test/ping",
      "weight": 100
    },
    {
      "id": "node-de-01",
      "label": "Frankfurt 01",
      "outboundTag": "pool-de-01",
      "probeUrl": "https://connectivity.example.test/ping",
      "weight": 90
    }
  ],
  "active": "node-sg-01"
}

Use a probe destination that you control or a stable endpoint intended for repeated checks. Public speed-test pages are poor health targets because they add redirects, dynamic content, rate limits, and geographic variation. A small HTTPS response with a known status code is easier to evaluate. The probe must also be routed through the candidate outbound; otherwise the controller may only be testing the direct local network.

Control plane

Listen
127.0.0.1
Port
10085
Purpose
Stats and runtime administration

Keep the API independent from the active proxy route.

Data plane

SOCKS
127.0.0.1:10808
HTTP
127.0.0.1:10809
Purpose
Application traffic and probes

The probe must explicitly use the candidate outbound path.

Policy configuration determines which statistics Xray collects. If you need per-outbound counters, make sure the policy level enables the required stats and that the outbound tags are stable. Avoid enabling verbose accounting for an unlimited number of short-lived dynamic tags; excessive metrics can make diagnosis harder and consume unnecessary resources.

Measure node health and score candidates

A practical health check should record more than one number. At minimum, collect success or failure, total request duration, connection time if available, HTTP status, and an error category. A timeout is different from a TLS verification failure, a refused connection, an HTTP 403 response, or a DNS error. Those categories help determine whether to retry, quarantine, or inspect configuration.

Run three consecutive probes before promoting a node, with a short interval such as two seconds. Three successful samples reduce the chance that a single lucky response activates a poor route. During normal operation, probe the active node every 15 to 30 seconds and test standby candidates less frequently, such as once every 60 seconds. Do not probe every node every second; that creates needless traffic and can trigger provider-side abuse controls.

Uses a real request through the candidate outbound and verifies both transport completion and an expected status code.

Suitable for: primary node selection and failover decisions

Checks whether a socket can connect, but does not validate TLS, routing, authentication, or application response.

Suitable for: fast preliminary filtering

Shows that traffic has passed through an outbound, but cannot prove that new requests will succeed.

Suitable for: observability and post-switch verification

A weighted score is easier to tune than a chain of arbitrary “if” statements. For example, reject any candidate with a failed probe, then calculate a score from latency, recent failure count, and operator-defined preference:

function score(sample, node) {
  if (!sample.ok) return -Infinity;

  const latencyPenalty = Math.min(sample.latencyMs / 10, 40);
  const failurePenalty = Math.min(sample.failures * 15, 45);
  const weightBonus = Math.min(node.weight / 10, 10);

  return 100 - latencyPenalty - failurePenalty + weightBonus;
}

This formula is a starting point, not a universal benchmark. If a 180 ms route is reliable and a 70 ms route fails twice a minute, the reliable route should usually win. Add hysteresis so that the controller does not switch merely because the new score is one or two points higher. A useful rule is to require the new candidate to beat the current node by at least 15 points, or require the current node to fail two consecutive active probes.

Conclusion: stability should outrank a small latency advantage

Use latency to rank healthy candidates, but use consecutive failures and a switch threshold to decide whether a route deserves replacement. A controller that changes nodes for every minor fluctuation usually performs worse than one that accepts an extra 20 to 40 ms for a stable path.

Build the controller and apply a switch

The safest implementation pattern is to keep a canonical configuration outside the running process, generate a candidate configuration from that source, validate it, and then apply a small, observable change. The controller should never edit a JSON file with blind string replacement. JSON formatting, duplicated tags, transport objects, and provider-specific fields make text replacement fragile.

  1. Load inventory

    Read the node list, active identifier, probe URL, cooldown state, and last known result from a local state file.

  2. Probe candidates

    Run three probes through each eligible outbound, record latency and error class, and reject nodes still inside quarantine.

  3. Choose route

    Calculate scores, apply hysteresis, and require a meaningful improvement before replacing a currently healthy node.

  4. Validate config

    Render the selected outbound reference, check duplicate tags, and run the Xray configuration test before applying it.

  5. Apply and verify

    Use the supported API operation or a controlled reload, then send one verification request and persist the new state only after success.

There are two broad ways to apply a change. The first keeps all candidate outbounds loaded and changes which one receives ordinary traffic through routing or balancer logic. This can provide a fast switch, but the exact runtime mutation available depends on the Xray build and the API services exposed by that build. The second renders a complete configuration with one selected outbound or selector policy and performs a controlled reload or restart. It is slower but easier to audit and reproduce.

Do not assume that every client’s “switch node” button calls the same Xray API method. v2rayN, v2rayNG, NekoBox, and other clients may manage profiles outside the core, may restart the core, or may use a different internal control path. A script intended for a standalone Xray process should communicate with the process it owns. If a graphical client owns the configuration, modifying its files while it is running can be overwritten during the next subscription update.

async function reconcile(state, inventory) {
  const candidates = inventory.nodes
    .filter(node => !isQuarantined(node.id, state))
    .map(node => ({
      node,
      sample: probeThroughOutbound(node.outboundTag)
    }));

  const healthy = candidates
    .filter(item => item.sample.ok)
    .map(item => ({
      ...item,
      score: score(item.sample, item.node)
    }))
    .sort((a, b) => b.score - a.score);

  if (healthy.length === 0) {
    return { action: "keep-current", reason: "no-healthy-candidate" };
  }

  const best = healthy[0];
  const current = healthy.find(item => item.node.id === state.active);

  if (current && best.node.id !== current.node.id) {
    if (best.score < current.score + 15 && !current.sample.hardFailure) {
      return { action: "keep-current", reason: "hysteresis" };
    }
  }

  await applySelectedOutbound(best.node.outboundTag);
  const verified = await verifyApplicationPath(best.node.outboundTag);

  if (!verified.ok) {
    quarantine(best.node.id, 120);
    return { action: "rollback", reason: verified.error };
  }

  return {
    action: "switched",
    active: best.node.id,
    score: best.score
  };
}

The probe function in this pseudocode must be explicit about routing. Sending the request to 127.0.0.1:10809 is not enough if the running configuration still chooses a random balancer member. For isolated candidate tests, use a temporary inbound or a candidate-specific routing rule, or run a short-lived test instance whose only proxy outbound is the candidate. A temporary instance avoids contaminating the active node’s statistics and makes the result easier to interpret.

Add recovery, cooldown, and observability

Failure recovery is where a simple switching script becomes an operational tool. When a node fails, mark the failure time, increment a counter, and place the node in quarantine. A quarantine period of two minutes prevents immediate re-selection while a transient outage is still occurring. After the quarantine expires, require fresh successful probes before the node can return to the pool.

  • Soft failure: one slow probe or one temporary timeout. Keep the active node, increase its penalty, and probe again.
  • Hard failure: repeated connection refusal, authentication failure, or three consecutive timeouts. Switch if another candidate passes validation.
  • Configuration failure: invalid JSON, duplicate outbound tags, or missing transport fields. Do not retry node selection; alert the operator and retain the last known-good configuration.
  • Verification failure: the new route applied but the real application request failed. Roll back to the previous active candidate and quarantine the new one.

Use a lock so that two scheduled runs cannot switch at the same time. A simple file lock or operating-system mutex is sufficient for a single machine. Store timestamps in UTC, include the controller version, and write the previous active node before applying a new one. If the process is interrupted during a switch, the next run can determine whether the active state is uncertain and perform a verification before making another change.

{
  "active": "node-sg-01",
  "previous": "node-de-01",
  "changedAt": "2026-08-29T09:42:18Z",
  "cooldownUntil": "2026-08-29T09:42:48Z",
  "nodes": {
    "node-sg-01": {
      "lastSuccess": "2026-08-29T09:42:25Z",
      "consecutiveFailures": 0,
      "latencyMs": 86
    },
    "node-de-01": {
      "lastFailure": "2026-08-29T09:41:57Z",
      "consecutiveFailures": 3,
      "quarantineUntil": "2026-08-29T09:43:57Z"
    }
  }
}

Log one structured record per probe and one record per decision. Include the node identifier, outbound tag, result, latency, status code, error category, old route, new route, and reason. Avoid writing subscription credentials, UUIDs, private keys, or complete URLs containing sensitive query parameters into logs. A useful record might say that node-de-01 failed three HTTPS probes with timeout errors and that the controller selected node-sg-01 after a score difference of 28 points.

Validate the workflow before production use

Start with a dry-run mode. In dry-run mode, the controller performs inventory loading, probes candidates, calculates scores, and prints the intended action without modifying routing or restarting Xray. Compare its decisions with manual observations for at least one day. This often reveals incorrect proxy-test assumptions, an unreachable probe destination, or a scoring function that rewards a fast but unreliable node.

Measures decisions without changing the active route. It is the safest way to tune thresholds and detect false positives.

Suitable for: initial deployment and score calibration

Applies decisions immediately and can reduce downtime, but a bad threshold can cause rapid route flapping.

Suitable for: monitored production environments

Creates a proposed switch for an operator to approve. It adds delay but is useful when traffic stability is more important than fast recovery.

Suitable for: sensitive or regulated networks

Test at least six scenarios: a healthy active node, a slow active node, a completely unreachable node, an invalid candidate configuration, a failed post-switch verification, and simultaneous controller runs. Also test subscription refresh separately from switching. Subscription data should update the inventory, while the controller should preserve the last known-good active state until the new nodes pass health checks.

Keep a static fallback route that is not managed by the same experimental logic. If every candidate fails, the controller should not generate an empty routing rule or repeatedly restart Xray. It should keep the previous configuration when possible, expose the failure in logs, and alert the operator. Repeated restarts can turn a temporary upstream outage into a local outage by interrupting otherwise usable connections.

Finally, document the ownership model. If v2rayN or another client manages the running core, let that client remain the source of truth or use its supported profile mechanism. If your script owns a standalone Xray process, keep the canonical JSON, inventory, state file, and service unit in one controlled directory. This prevents the common situation where an automation script selects one node, a subscription update rewrites the profile, and the next restart silently restores a different route.

A dependable Xray API switcher is therefore less about calling one command and more about building a disciplined control loop: measure through the intended outbound, score only comparable results, require evidence before switching, validate every change, and preserve a known-good rollback. With those boundaries in place, API statistics and runtime controls become useful building blocks for automation rather than another source of unpredictable route changes.

Download v2rayN