This guide shows how to build a maintainable Xray node failover controller instead of changing the active node manually. It explains outbound tags, routing selectors, the Xray API service, health-check design, latency scoring, JSON configuration boundaries, and a conservative script workflow that can replace or reload a node without disrupting unrelated traffic.
What Xray node failover actually means
Xray does not automatically turn a list of imported nodes into a reliable failover system merely because several outbounds exist in the JSON file. An outbound is a named connection definition, while failover is a decision process: the system must test candidates, determine whether a candidate is usable, select an alternative, and apply that decision to new traffic. The client interface may expose these operations as “select server” or “auto test,” but an API-based setup requires the same logic to be made explicit.
A practical design separates the data plane from the control plane. Xray remains responsible for accepting local traffic, applying routing rules, opening proxy connections, and maintaining active sessions. A small external controller acts as the control plane. It reads a list of candidate outbound tags, performs checks through the local proxy or a dedicated test path, calculates a score, and then changes the preferred route. This separation makes failures easier to diagnose because a bad node, a failed API request, and a malformed configuration are different events.
The word “failover” should also be interpreted carefully. Switching the preferred outbound normally affects new connections. Existing TCP streams, WebSocket sessions, downloads, and long-lived application connections may continue using the old outbound until they close or time out. A script should therefore avoid claiming that every connection moves instantly. The safer expectation is that new requests use the replacement node while old sessions drain naturally.
For most installations, keep at least one stable direct or management path outside the failover group. The API listener should not depend on the proxy node that the script is currently testing. If the only route to the API is itself broken, the controller cannot recover the system even when another node is healthy.
Conclusion: failover is a policy, not a protocol
VLESS, VMess, Trojan, and other outbound protocols describe how Xray connects to a server. They do not decide when that server should be abandoned. Put availability thresholds, cooldown periods, and switching rules in the controller rather than hiding them inside node definitions.
Design outbound tags and routing selectors first
Every candidate node needs a stable and unique tag. Do not use display names copied from a subscription as the only identifier; providers may rename nodes, include duplicate labels, or add characters that complicate shell and JSON handling. A predictable naming scheme such as pool-us-01, pool-us-02, and pool-jp-01 gives the script an unambiguous target. Keep the tag unchanged when other settings, such as the address or port, are updated.
A simple configuration can route selected traffic to a fixed outbound tag:
{
"outbounds": [
{
"tag": "pool-us-01",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "edge-01.example.net",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality"
}
},
{
"tag": "direct",
"protocol": "freedom"
},
{
"tag": "block",
"protocol": "blackhole"
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"domain": ["geosite:category-ads-all"],
"outboundTag": "block"
},
{
"type": "field",
"network": "tcp,udp",
"outboundTag": "pool-us-01"
}
]
}
}
This example is intentionally static. The important part is the relationship between routing.rules[].outboundTag and outbounds[].tag. The strings must match exactly, including capitalization. A script that changes a tag in one place but not the other can produce a valid-looking JSON file that sends traffic to a nonexistent outbound or falls through to an unintended rule.
For a larger pool, use a balancer instead of rewriting many domain rules. A balancer can group selectors and let routing refer to a single logical name. The controller can then maintain the preferred candidate or regenerate the balancer selector. However, verify the exact API and core version in use before assuming a balancer can be altered live. Xray’s API surface differs by service and version, and a configuration reload is often more predictable than an unsupported hot mutation.
Keep candidate tags in a logical group and let the controller update the preferred selection or generated configuration. This avoids duplicating routing rules for every node.
Suitable for: multiple applications and several node regions
Route normal traffic to one tag and have the script replace that tag’s target after a health decision. The model is easy to understand but requires careful reload handling.
Suitable for: one traffic class and small deployments
Use one pool for each region or application class, such as work traffic and general browsing. A failure in one pool does not necessarily move every request.
Suitable for: operators with regional routing policies
Expose only the Xray API services you need
The Xray API is a gRPC-based management interface. It is not the same thing as the local HTTP or SOCKS proxy port. A common mistake is to point a script at port 10808 and expect management methods to be available there; that port is normally a proxy inbound, not the API endpoint. Create a dedicated API inbound, bind it to loopback, and enable only the required services in the api section.
A minimal management structure may look like this:
{
"log": {
"loglevel": "warning"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"StatsService",
"RoutingService"
]
},
"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 service list depends on the controller. StatsService can expose counters when statistics are enabled, HandlerService provides handler-related management operations supported by the running core, and RoutingService exposes routing operations available in that version. The API tag is an outbound-style internal destination, so the API inbound must be routed to that tag. If this rule is missing, the listener may exist while requests are sent through an ordinary outbound.
Bind the API to 127.0.0.1 unless a separate management host genuinely needs access. The Xray API does not replace network authentication or transport security. Exposing an unauthenticated management port on 0.0.0.0 can allow another machine to inspect statistics or alter handlers. If remote administration is unavoidable, restrict it with a firewall, private management network, and an authenticated tunnel rather than forwarding the port directly to the public Internet.
- Use a dedicated port such as
10085, and confirm that no other service already listens there. - Keep the API inbound separate from the normal SOCKS port
10808and HTTP port10809. - Record the Xray core version and generated API definitions used by the controller.
- Test the API locally before enabling automatic switching.
- Log every attempted change with the old tag, new tag, reason, and configuration checksum or timestamp.
Build health checks that measure usefulness
A node that answers a TCP connection is not necessarily a good replacement. Health checks should measure the path that matters to the operator: DNS resolution, TLS or protocol handshake, response time through the proxy, and optionally a small application request. Do not download a large file on every cycle. A lightweight HTTPS request with a short timeout usually provides enough information for availability and latency decisions.
Run checks through each candidate’s own outbound. Testing the destination directly only proves that the local network can reach the destination; it does not prove that Xray can establish the candidate tunnel. A controller can create a temporary local test path for each tag, use an existing per-node inbound, or run a controlled request through the configured proxy selection. The implementation depends on the client architecture, but the measurement must include the candidate outbound rather than the direct route.
| Signal | Example threshold | Interpretation | Controller action |
|---|---|---|---|
| Handshake success | Required | The node can complete the protocol connection | Reject the sample if it fails |
| Proxy latency | Under 800 ms | Measures response time through the selected node | Add a weighted latency score |
| Consecutive failures | 3 checks | Reduces reactions to one transient timeout | Quarantine the candidate temporarily |
| Recovery checks | 2 successes | Prevents immediate oscillation after recovery | Return the node to the eligible pool |
Latency scoring should use more than the lowest single result. A useful score can combine the median of three samples, failure penalty, and a small preference for the currently active node. For example, a controller might calculate score = median_latency + failure_count * 1000. If two candidates are within 50 ms, keep the current node rather than switching for a negligible gain. This hysteresis is important: without it, normal jitter can cause repeated changes, broken sessions, and confusing logs.
Set separate values for connect timeout, request timeout, failure threshold, recovery threshold, and minimum hold time. A 2-second connect timeout and a 30-second hold time may suit a desktop network; a busy server with occasional congestion may need a longer request timeout. These numbers are operational policy, not universal Xray defaults. Record the results so that a later operator can distinguish a genuine outage from an overly aggressive script.
Implement the switching workflow safely
Start with a dry-run controller. It should read the candidate list, perform checks, print the score table, and state which node it would select without changing Xray. Once the measurements are believable, add a guarded apply phase. The apply phase should verify that the selected tag exists, the generated JSON parses successfully, the API or reload operation returns success, and the active route is confirmed by a follow-up request.
Define candidate tags
Store tags such as
pool-us-01andpool-us-02in a separate controller configuration. Do not discover arbitrary outbounds from untrusted input.Run three probes
Send three small requests through each candidate, with a 5-second per-request timeout and a 1-second pause between samples. Record success, latency, and error text.
Apply hysteresis
Require three consecutive failures before leaving the active node, two successful checks before restoring a quarantined node, and a 30-second minimum hold time between switches.
Validate the target
Before changing the route, confirm that the winning tag is present and that its protocol, server address, port, and user settings are complete.
Change or reload
Use a supported API operation for the installed core, or generate a complete configuration and perform a validated reload. Never assume an arbitrary JSON fragment can be injected live.
Verify and record
Send a post-switch request, compare the observed route with the selected tag, and write the old tag, new tag, score, reason, and result to a durable log.
There are two practical ways to apply a decision. The first is an API mutation: where the installed Xray version supports the required HandlerService or routing operation, the controller can update a handler or routing object without replacing the entire process. This can reduce disruption, but the method and request structure must match the core’s actual API definitions. The second is configuration regeneration followed by a controlled reload or restart. It is slower, yet often easier to audit because the resulting JSON is a complete file that can be validated before activation.
Do not rewrite the entire configuration on every health-check cycle. Separate static data, such as inbound listeners and API settings, from a small generated selection file or a clearly marked outbound section. Write to a temporary file, parse it, make a backup of the last known-good version, then replace the active file atomically where the operating system permits. If validation fails, keep the old configuration and mark the controller run as unsuccessful.
{
"controller": {
"activeTag": "pool-us-01",
"intervalSeconds": 30,
"connectTimeoutSeconds": 2,
"requestTimeoutSeconds": 5,
"failureThreshold": 3,
"recoveryThreshold": 2,
"minimumHoldSeconds": 30,
"dryRun": false
},
"candidates": [
{
"tag": "pool-us-01",
"weight": 100,
"region": "us"
},
{
"tag": "pool-us-02",
"weight": 90,
"region": "us"
},
{
"tag": "pool-jp-01",
"weight": 80,
"region": "jp"
}
]
}
This controller metadata is not an Xray core configuration by itself. Keep that distinction explicit. Xray will ignore arbitrary top-level fields if they are placed incorrectly, while a strict parser may reject them. The controller should load its own policy file, load the Xray JSON separately, and generate only the Xray structures that the core understands.
Prevent flapping, deadlocks, and unsafe recovery
The most damaging automation bug is flapping: the script changes from node A to node B, sees a temporary improvement, then immediately switches back. Use a minimum hold time, a margin between scores, and a quarantine state. A candidate with one failed probe should be “degraded,” not immediately removed. A candidate with three consecutive failures can enter quarantine for 60 seconds, after which the controller performs a recovery test.
Keep a last-known-good node and a clear emergency policy. If every candidate fails, do not repeatedly rewrite the configuration. Keep the last active node for a limited period, route management traffic directly where appropriate, and raise an alert. Repeated reloads can make the outage worse by interrupting API access, exhausting file descriptors, or creating overlapping core processes.
Conclusion: the fallback must be boring
A reliable controller should make fewer changes during an outage, not more. One stable last-known-good tag, one bounded retry policy, and one explicit alert are safer than a loop that rotates through every node without waiting for evidence.
Also distinguish node failure from destination failure. If every candidate times out against the same test endpoint, the endpoint, DNS path, or local network may be the problem. If only one candidate fails while the others succeed, quarantine that candidate. If health checks pass but applications fail, inspect routing rules, DNS strategy, UDP behavior, MTU, and application-specific proxy bypasses before blaming the failover algorithm.
Can Xray automatically choose the fastest node from JSON alone?
No. JSON defines outbounds, routing, and optional balancer structures, but a useful fastest-node policy needs measurements and decision logic. Use Xray’s supported balancing or observation features where appropriate, or let an external controller calculate scores and apply a validated selection.
Is port 10085 the normal proxy port?
No. In this example, 10085 is a loopback API listener. Keep ordinary SOCKS and HTTP proxy ports separate, commonly 10808 and 10809, and use the values shown in the current configuration rather than assuming defaults.
Will switching move existing downloads to the new node?
Usually not. New connections can use the new outbound, while existing streams remain attached to the old connection until they close. Applications with retry support may reconnect through the new route.
Should the API listen on all network interfaces?
Normally no. Bind it to 127.0.0.1 and let the local controller access it. If remote management is required, protect the path with firewall rules and an authenticated private tunnel instead of exposing the API directly.