This guide is for operators who want to monitor Xray traffic and automate decisions without repeatedly editing a client’s node list by hand. It explains how to enable the local gRPC API, read StatsService counters, and use RoutingService balancer overrides for controlled node selection. You’ll also see where HandlerService fits, how to protect the API, and how to avoid switching nodes based on noisy or misleading measurements.
What the Xray gRPC API Can—and Cannot—Do
The Xray API is a control interface for a running core. It is not a subscription service, a replacement for client configuration, or a mechanism that automatically discovers whether a server is healthy. Your application must supply the decision logic: query measurements, compare them against a policy, and ask Xray to perform an operation when the evidence is strong enough.
Three services are especially relevant to automated node management. StatsService reports traffic counters when statistics are enabled. HandlerService manages runtime handlers, including adding, removing, or altering supported inbounds and outbounds. RoutingService provides routing-related operations, including overriding a balancer target in cores that support the relevant API method. The installed Xray binary and its API definitions determine which methods are available; do not assume that every fork or older build exposes an identical API.
A useful distinction is that HandlerService changes runtime handlers, while a balancer override changes which existing outbound a balancer selects. If the goal is to switch among a fixed set of nodes, a routing balancer is often a cleaner design than repeatedly deleting and recreating outbounds. HandlerService can still be useful for controlled runtime changes, but it does not magically rewrite every routing rule or subscription entry.
- Use statistics for observation: counters describe traffic handled by the core; they do not directly report server CPU load, packet loss, or the capacity of a remote exit.
- Use routing controls for selection: define candidate outbounds and a balancer in the configuration, then select among those candidates using the API supported by your build.
- Keep configuration ownership clear: a desktop client may generate and replace the runtime configuration. Test API changes against the configuration actually launched by the core, not an unrelated file on disk.
Configure a Local API Inbound and Statistics
The API is reached through an inbound that accepts the local gRPC connection. A common example uses 127.0.0.1:10085, the dokodemo-door protocol, and a routing rule that sends requests entering the API inbound to the API outbound. The address and port below are examples; check that port 10085 is unused and make sure your client connects to the same address and port.
{
"api": {
"tag": "api",
"services": [
"HandlerService",
"RoutingService",
"StatsService"
]
},
"stats": {},
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true
}
},
"inbounds": [
{
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api"
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "api"
}
],
"routing": {
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
}
]
}
}
This is a configuration example, not a universal drop-in replacement for a client-generated file. If the active configuration already has an API inbound, API outbound, or routing rules, merge the required fields rather than adding duplicate tags. Tags must be unique where the configuration requires them to be, and the API routing rule must not be shadowed by an earlier rule. Restart or reload the core using the method supported by the application, then inspect startup logs for JSON errors, a port bind failure, or an API service initialization problem.
The stats object enables the statistics facility, while policy flags determine which categories of traffic are counted. System-level inbound counters and user-level counters serve different purposes. Inbound counters can help compare traffic entering particular listeners; user counters require the relevant user identity and policy level to be configured. A counter that is absent is not necessarily evidence of zero traffic: first check whether the matching statistics option is enabled and whether that traffic has a tag or user identity that can be counted.
Local API access
- Example host
- 127.0.0.1
- Example port
- 10085
- Inbound protocol
- dokodemo-door
- API tag
- api
Keep the listener on loopback unless you have a separately designed, secured management network.
Traffic counters
- Facility
- StatsService
- Global switch
- stats: {}
- System flags
- statsInboundUplink / Downlink
- User flags
- statsUserUplink / Downlink
Enable only the counter categories your monitoring workflow needs.
For node selection, configure candidate outbounds with stable, distinct tags such as node-a and node-b. A balancer’s selector should match only the intended candidates, and a routing rule should direct the traffic you want to manage to that balancer. Keep the API inbound rule separate: API control requests must reach the API handler, not be sent through the node-selection balancer. Check your Xray configuration format and core build before copying balancer fields, since supported routing options can differ between core versions.
Build a Repeatable Node-Selection Workflow
Start with a manual baseline before automating changes. Verify that each candidate outbound works on its own, that the intended user traffic matches the balancer rule, and that API calls can reach the local listener. Only then add a controller. The steps below describe a cautious workflow; menu labels for enabling a custom core configuration vary between client versions, so confirm that the running core is actually using the file you edited.
-
Confirm the core
Run
xray versionfor the binary you intend to control, and compare it with the executable path shown in the client’s core startup log. Confirm that the build exposes the API services and methods your controller needs. -
Define candidates
Assign each outbound a unique tag, such as
node-a,node-b, andnode-c. Configure a balancer selector that matches only these tags, then route the relevant traffic to the balancer. Keep direct, block, and API outbounds outside the candidate set. -
Enable counters
Add
StatsService, thestatsobject, and the policy flags for the counters you will read. Start Xray and check the log for configuration errors. Send a small, known amount of test traffic before treating an empty result as a monitoring failure. -
Read and compare
Connect to
127.0.0.1:10085and query counters at a fixed interval, for example every 10 seconds. Record each counter’s name and value with a timestamp. Compare changes between samples rather than comparing lifetime totals from nodes that have been active for different lengths of time. -
Switch with a guard
When the installed Xray API supports balancer target overrides, call the corresponding RoutingService method for the configured balancer and a valid candidate tag. Log the requested target, API response, and time. Confirm that new connections use the target; existing connections may continue on their original outbound.
StatsService exposes named counters rather than a ready-made “best node” score. Common counter names identify a scope and direction, for example outbound>>>node-a>>>traffic>>>uplink or outbound>>>node-a>>>traffic>>>downlink. User counters may include a user identifier. Treat the returned counter name as an identifier, not as a guaranteed schema across all builds. Your collector should tolerate missing counters, new names, and nodes that have not yet carried traffic.
- Calculate deltas: if a counter rises from 4,000,000 to 5,200,000 bytes over 10 seconds, the observed average is about 120,000 bytes per second for that interval. This is traffic volume, not a direct measurement of maximum node throughput.
- Handle resets: counters may restart when the core restarts, and a query option that resets counters can make later samples incomparable with earlier ones. Avoid resetting in a shared monitoring workflow unless the collector deliberately owns that measurement window.
- Separate traffic from health: a node with no observed bytes may simply be idle. Use an explicit connectivity probe or a controlled test request if reachability is part of the selection policy.
- Preserve history: store timestamps and counter values outside the Xray process if you need trends across restarts. A process restart can erase in-memory observations.
Design Selection Logic That Does Not Flap
Traffic counters help answer “how much data passed through this tagged outbound?” They do not, by themselves, answer “which node is fastest right now?” A busy node can have a large counter because it has served more traffic, while an idle node can have a small counter despite having excellent latency. For active selection, combine counter deltas with a deliberate health check and, where useful, a repeatable latency or transfer test.
Keep the controller’s policy small and observable. For example, collect samples every 10 seconds, require three consecutive failed probes before marking a candidate unhealthy, and wait at least 60 seconds after a switch before making another automatic switch. These are starting values, not universal thresholds: increase the observation window on unstable networks and test against the actual traffic pattern. Do not select a new node because of one delayed response or one short-lived traffic burst.
Decision rule: Require evidence before moving traffic
Switch only when the current node fails a defined health condition for several consecutive checks, or when a measured alternative is consistently better across multiple samples. Use a cooldown and keep the previous target available so that a failed change can be reversed quickly.
A practical controller can use a simple state machine: healthy, suspect, unhealthy, and recovering. One failed probe moves a node to suspect; repeated failures move it to unhealthy. A successful probe should not immediately restore it to healthy if the service is still unstable—require several successful checks over a recovery window. This prevents a node that alternates between working and failing from repeatedly taking traffic away and receiving it back.
Use the RoutingService balancer override for selecting among already configured candidates when supported by the running core. Confirm the actual balancer tag and target tag, and record errors instead of silently assuming a successful switch. A target accepted by the API still does not prove that user traffic matches the intended balancer rule. Verify with a new connection and, if available, a controlled request whose destination and expected route are known.
HandlerService is a separate option for runtime handler management. Adding or removing an outbound can be useful for a system that creates candidates dynamically, but it increases the number of moving parts: the controller must provide a valid operation for the outbound protocol, preserve tag consistency, and account for active connections and routing references. Do not remove an outbound merely because a counter is currently zero. It could be idle but still needed, or the counter might be disabled. For a fixed subscription-based node list, keep candidates in the configuration and switch the balancer target instead.
Secure the API and Troubleshoot Failures
The local API is a powerful control surface. A loopback listener is a safer default because it limits access to processes on the same host, but it is not an authentication system: another local process with access to the port may be able to issue control requests. Do not bind the API to 0.0.0.0 or a public interface for convenience. If remote management is necessary, put it behind a carefully controlled private management path with firewall restrictions and appropriate transport protection; do not assume the Xray API inbound itself provides encryption or access control.
- Run the controller under a dedicated operating-system account where practical, and restrict access to its configuration, logs, and credentials.
- Allow only the API services the workflow needs. If the controller only reads counters and selects a balancer target, do not expose unrelated control methods without a reason.
- Log method, target, timestamp, and outcome, but do not log subscription URLs, authentication material, or full configuration secrets.
- Rate-limit polling and retries. A 10-second statistics interval is usually a more useful starting point than issuing rapid continuous queries.
When a request fails, identify which layer is failing before changing node settings. A gRPC connection error often points to a listener, port, or service mismatch. A successful connection with no matching counters points toward disabled policy flags, an unexpected tag, or no traffic in the selected scope. A successful balancer request with no visible route change points toward the balancer configuration, routing-rule order, or reuse of an existing connection.
- Connection refused: confirm Xray is running, the API inbound is listening on the expected loopback address, and port 10085 is not occupied by another process.
- Unknown service or method: check the exact core binary and its available API definitions. A client generated for a different API version may not match the running core.
- Empty statistics response: verify
StatsService,stats, and the relevant policy flags, then generate test traffic through the tagged outbound. - Target change appears ineffective: check that the target belongs to the balancer’s candidate set, that the intended routing rule uses that balancer, and that you tested a new connection.
Frequently Asked Questions
Can StatsService tell me which node has the least load?
No. It reports configured traffic counters, not remote CPU usage or a universal load score. Compare counter deltas with explicit health checks and controlled measurements; an idle counter alone does not mean a node is better or worse.
Does switching the balancer target move existing connections?
Do not assume it does. A target change affects routing decisions made after the change; established connections may remain attached to their original outbound. Test with a new connection and retain a rollback target.
Should I use HandlerService to replace nodes after every check?
Usually not for a fixed set of subscription nodes. Define candidate outbounds and use a supported balancer override. Reserve HandlerService operations for cases that genuinely need runtime handler creation or alteration.
Can I expose port 10085 so another machine can manage Xray?
A loopback-only listener is the safer default. Do not expose the control port directly to an untrusted network. If remote management is required, design a restricted, protected management path and verify firewall rules before enabling it.