Clash API Node Switching: Advanced Scripting Guide
Learn how to turn Clash into an automated proxy failover system. You will build scripts that test node health, switch strategy groups, record failures, and protect the controller endpoint with practical YAML and API examples.
Clash can already switch between proxies manually, but the controller API makes it possible to build a repeatable failover workflow around the core. A small script can inspect the proxy list, test each node through a real URL, select a healthy node in a strategy group, and record enough information to explain why a switch occurred. This is useful on a workstation that must stay connected, a small home gateway, or a server where a graphical client is not always open.
This guide uses the mihomo-compatible controller API exposed by current Clash-based clients. The same concepts generally apply to Clash Verge Rev, Clash Plus, FlClash, ClashX Meta, and other clients that expose an external controller. Endpoint names, default ports, and GUI labels can differ, so always verify the actual controller address and authentication settings in the client before running a script.
Security First
The external controller is an administrative interface, not a normal proxy port. Keep it bound to 127.0.0.1 unless remote administration is genuinely required. Never expose an unauthenticated controller to the public internet, and treat the controller secret like a password.
How Clash API Failover Works
A failover script needs four separate pieces: a controller endpoint, a group containing candidate proxies, a health-check method, and a decision rule. The controller provides the current runtime state. The group defines which proxies are eligible. The health check measures whether a proxy can reach a chosen test URL. The decision rule determines when the script should keep the current node, switch to another node, or declare the whole group unhealthy.
Clash's configuration API is commonly enabled with an external-controller address and an optional secret. A minimal local configuration looks like this:
external-controller: 127.0.0.1:9090
secret: replace-with-a-long-random-secret
Some clients write this setting into a generated configuration rather than the profile file you edit. If a GUI owns the active configuration, changing a downloaded profile may have no effect after the next refresh. In that situation, use the client's controller or external-controller settings page, then confirm the value by making a local API request.
curl -H "Authorization: Bearer replace-with-a-long-random-secret" \
http://127.0.0.1:9090/version
A successful response normally contains the core version and a meta version field. A connection refusal means the controller is disabled, the address is different, or the client is listening on another interface. A 401 response usually means the secret is missing or does not match. Test this basic endpoint before debugging proxy groups or health checks.
The main runtime endpoint is GET /proxies. It returns a JSON object containing proxy entries and groups. A group usually includes fields such as type, now, all, and sometimes delay or provider-related information. The exact response can vary by core version, so scripts should check for fields defensively instead of assuming every proxy has the same structure.
| API operation | Purpose | Typical method |
|---|---|---|
/version | Confirm that the controller is reachable | GET |
/proxies | Read nodes, groups, current selections, and available members | GET |
/proxies/{name} | Read one proxy or group | GET |
/proxies/{name}/delay | Test a proxy against a URL with a timeout | GET |
/proxies/{group} | Select a member in a selectable group | PUT |
The proxy name in a URL path must be percent-encoded. Names containing spaces, slashes, brackets, or non-ASCII characters are common in subscription lists. A script that concatenates the raw name directly into /proxies/... will eventually fail on a perfectly valid node name.
Prepare a Switchable Strategy Group
The script should not rewrite individual proxy definitions. It should select a member of an existing group. This preserves the profile's node parameters, routing rules, DNS settings, and subscription refresh behavior. A simple selectable group can be added to a YAML configuration like this:
proxy-groups:
- name: AUTO-FAILOVER
type: select
proxies:
- Node-A
- Node-B
- Node-C
- DIRECT
The names under proxies must match the actual proxy names exactly. They are not aliases, and YAML punctuation is part of the name. If a subscription provider changes node names during refresh, the static group may lose members. In that case, use a provider-backed group or generate the group from a stable naming convention, but inspect the resulting runtime list through /proxies before automating it.
A fallback group is useful when the core itself should perform periodic checks:
proxy-groups:
- name: AUTO-FALLBACK
type: fallback
url: https://www.gstatic.com/generate_204
interval: 300
proxies:
- Node-A
- Node-B
- Node-C
The fallback group can automatically prefer a responsive member, while a select group gives an external script explicit control. Do not mix both mechanisms casually. If a script selects a node in a group whose type continuously performs its own selection, the core may change the choice again on the next health-check cycle. Choose one authority for each group: either the core's fallback or URL-test logic, or your own script.
For a rule-based configuration, route traffic through the group name rather than through a specific node:
rules:
- DOMAIN-SUFFIX,example.com,AUTO-FAILOVER
- MATCH,AUTO-FAILOVER
This separation is important. The API changes the active member of AUTO-FAILOVER; it does not need to touch the rules. Existing connections may not all migrate instantly. Long-lived TCP sessions can remain attached to the old path until they reconnect, while new connections normally use the newly selected member.
Choose a Reliable Test URL
A health check should measure the service you actually care about. A small HTTPS endpoint that returns a quick success response is generally better than downloading a large web page. https://www.gstatic.com/generate_204 is frequently used for connectivity tests, but it may not be suitable in every network environment. A provider's status endpoint, an internal service, or a stable endpoint close to the expected destination can produce more meaningful results.
One successful request is not proof that a node is good for every destination. A node can reach the test URL while failing to connect to a particular streaming service, DNS resolver, or region. For important deployments, use several test URLs or perform a second application-specific check after the basic proxy test passes.
Avoid an Overly Aggressive Threshold
Internet latency naturally varies. A script that switches after one timeout can oscillate between nodes whenever a Wi-Fi network briefly drops a packet. Use a timeout, consecutive-failure threshold, cooldown period, and minimum switch interval together.
Build a Python Node Health and Switching Script
The following example uses only Python's standard library. It reads the current group from /proxies, tests each member through the controller's delay endpoint, ignores entries such as DIRECT when requested, and changes a selectable group only when the current member is unhealthy or a clearly better candidate is available.
#!/usr/bin/env python3
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
CONTROLLER = os.getenv("CLASH_CONTROLLER", "http://127.0.0.1:9090")
SECRET = os.getenv("CLASH_SECRET", "")
GROUP = os.getenv("CLASH_GROUP", "AUTO-FAILOVER")
TEST_URL = os.getenv(
"CLASH_TEST_URL",
"https://www.gstatic.com/generate_204"
)
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT_MS", "5000"))
SWITCH_MARGIN_MS = int(os.getenv("CLASH_SWITCH_MARGIN_MS", "150"))
IGNORE = {"DIRECT", "REJECT"}
LOG_FILE = os.getenv("CLASH_FAILOVER_LOG", "clash-failover.jsonl")
def request_json(path, method="GET", payload=None):
url = CONTROLLER.rstrip("/") + path
headers = {"Accept": "application/json"}
if SECRET:
headers["Authorization"] = "Bearer " + SECRET
body = None
if payload is not None:
body = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
url, data=body, headers=headers, method=method
)
with urllib.request.urlopen(request, timeout=TIMEOUT_MS / 1000) as response:
raw = response.read().decode("utf-8")
return json.loads(raw) if raw else {}
def get_proxies():
return request_json("/proxies").get("proxies", {})
def encode_name(name):
return urllib.parse.quote(name, safe="")
def test_proxy(name):
query = urllib.parse.urlencode({
"timeout": TIMEOUT_MS,
"url": TEST_URL
})
path = "/proxies/" + encode_name(name) + "/delay?" + query
started = time.monotonic()
try:
result = request_json(path)
delay = result.get("delay")
if isinstance(delay, int):
return delay
except (urllib.error.URLError, urllib.error.HTTPError,
TimeoutError, ValueError):
pass
elapsed = int((time.monotonic() - started) * 1000)
return None if elapsed >= TIMEOUT_MS else elapsed
def select_proxy(group_name, proxy_name):
path = "/proxies/" + encode_name(group_name)
request_json(path, method="PUT", payload={"name": proxy_name})
def write_event(event):
event["time"] = int(time.time())
with open(LOG_FILE, "a", encoding="utf-8") as handle:
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
def main():
proxies = get_proxies()
group = proxies.get(GROUP)
if not isinstance(group, dict):
raise RuntimeError("Group not found: " + GROUP)
candidates = [
name for name in group.get("all", [])
if name not in IGNORE and name in proxies
]
if not candidates:
raise RuntimeError("No testable members in group: " + GROUP)
current = group.get("now")
results = {}
for name in candidates:
results[name] = test_proxy(name)
print(f"{name}: {results[name]} ms"
if results[name] is not None else f"{name}: failed")
healthy = {
name: delay for name, delay in results.items()
if isinstance(delay, int)
}
if not healthy:
write_event({
"group": GROUP,
"current": current,
"action": "no-healthy-node",
"results": results
})
raise RuntimeError("Every candidate failed")
best = min(healthy, key=healthy.get)
current_delay = healthy.get(current)
should_switch = (
current not in healthy or
healthy[best] + SWITCH_MARGIN_MS < current_delay
)
if should_switch and best != current:
select_proxy(GROUP, best)
action = "switched"
else:
action = "kept"
write_event({
"group": GROUP,
"current": current,
"selected": best,
"action": action,
"results": results
})
print(f"{action}: {current} -> {best}")
if __name__ == "__main__":
try:
main()
except Exception as exc:
print("error:", exc, file=sys.stderr)
sys.exit(1)
Before running it, export the controller values in the shell rather than placing the secret directly in the source file:
export CLASH_CONTROLLER="http://127.0.0.1:9090"
export CLASH_SECRET="replace-with-your-controller-secret"
export CLASH_GROUP="AUTO-FAILOVER"
python3 clash_failover.py
The script first obtains the group's current member and available members. It then calls the delay endpoint separately for each candidate. The API's delay response is not a general bandwidth benchmark; it reports the measured response time for the requested URL. A missing or invalid delay is treated as a failed check.
The switch condition has two protections. If the current node fails, the script can move to the best healthy candidate. If the current node works, another candidate must be faster by at least SWITCH_MARGIN_MS. Without that margin, small random differences cause unnecessary switching. The JSON Lines log records the current selection, every measured result, and the action taken, which makes later troubleshooting much easier than relying on a terminal message that has already disappeared.
Test the API Before Switching
Run the script manually several times while watching the active group in the client. First confirm that the group name appears in /proxies. Next test one encoded proxy name directly. Finally verify that the group changes only when the expected condition is met.
curl -G \
-H "Authorization: Bearer replace-with-your-controller-secret" \
--data-urlencode "timeout=5000" \
--data-urlencode "url=https://www.gstatic.com/generate_204" \
"http://127.0.0.1:9090/proxies/Node-A/delay"
curl -X PUT \
-H "Authorization: Bearer replace-with-your-controller-secret" \
-H "Content-Type: application/json" \
-d '{"name":"Node-B"}' \
"http://127.0.0.1:9090/proxies/AUTO-FAILOVER"
Use the PUT request only against a group that accepts manual selection, such as a select group. A group may reject the operation or ignore the selection if its type is not designed for manual switching. Also remember that an API success response only confirms that the controller accepted the request; check GET /proxies afterward to confirm the group's now value.
Prevent Flapping and Record Real Failures
Node switching is a control problem, not just a connectivity test. A robust automation loop needs hysteresis: the conditions for leaving a node should be stricter than the conditions for considering a node healthy again. Otherwise, a node that alternates between 4-second responses and timeouts can cause repeated selection changes, interrupting more traffic than it saves.
- Require consecutive failures. Mark the current node as unavailable only after two or three failed checks. A single timeout can be caused by local Wi-Fi loss, a sleeping laptop, or a temporarily busy server.
- Use a cooldown period. After a switch, wait before switching again. A cooldown of 60 to 300 seconds is a reasonable starting point for a desktop or home gateway.
- Keep a latency margin. Do not replace a stable 180 ms node merely because another node returned 170 ms once. A margin of 100 to 200 ms, or a percentage-based margin, reduces pointless changes.
- Separate health from preference. A lower delay does not always mean a better route. Region, protocol compatibility, packet loss, and destination access may matter more than a single response time.
- Log every decision. Store timestamps, group names, selected nodes, test URLs, delays, and error types. Avoid logging subscription URLs, bearer secrets, or complete configuration files.
For scheduled execution, run the script at a moderate interval rather than every few seconds. A systemd timer, Windows Task Scheduler task, or cron entry can execute it every one to five minutes. The script should also use a lock so two overlapping runs cannot send contradictory PUT requests. If the health check takes longer than the schedule interval, the next run should exit or wait instead of starting a second scan.
A production version should distinguish API errors from proxy failures. A 401 response means an authentication problem and should not trigger a node switch. A 404 for a proxy name can mean that the subscription changed. A timeout while calling the controller itself suggests that the Clash process or local machine is unavailable. These conditions need different alerts and recovery actions.
A Safer Decision Sequence
Read the group, test the current member twice if necessary, test alternatives, apply the cooldown rule, send one PUT request, read the group again, and then record the confirmed result. This sequence prevents the log from claiming a switch when the controller rejected or later overwrote the selection.
Protect and Operate the Controller Safely
The controller can change active proxies, reload configurations, and expose runtime information. Binding it to all interfaces with 0.0.0.0:9090 makes it reachable from the LAN and potentially from a forwarded or misconfigured firewall port. If a phone or another computer does not need to administer Clash, use 127.0.0.1 and run the script on the same machine.
If remote administration is required, restrict access at multiple layers: bind to the private LAN address rather than every interface, enable a strong secret, limit the operating-system firewall to trusted source addresses, and avoid forwarding the controller port from a router. Do not confuse allow-lan: true with safe controller exposure. allow-lan controls proxy listener access; it does not by itself protect the external controller.
Keep the controller secret outside shared scripts and repositories. Environment variables are convenient for local testing, while a file readable only by the service account is better for a long-running task. On Windows, use the Task Scheduler credential or a protected environment configuration. On Linux and macOS, restrict the secret file with appropriate filesystem permissions and avoid printing the full request command in verbose logs.
The script should also fail closed. If the controller cannot be authenticated, it should stop rather than attempt a fallback request without a secret. If every candidate fails, leaving the current group unchanged may be preferable to selecting DIRECT, especially when the automation exists to enforce a proxy route. If direct access is an intentional emergency path, put it in a separate group policy and make that choice explicit in the configuration.
When deploying this workflow in a GUI client, remember that profile updates can replace locally edited YAML. Keep the custom group in a persistent override or merge section supported by the client, and confirm after each subscription refresh that the group still contains the intended members. A correct API script cannot select a node that the active runtime configuration no longer exposes.
Finally, test failure modes deliberately: stop the current node, block the test URL, rotate the controller secret, rename a candidate, and restart the Clash client. Check whether the script reports the correct cause, whether it avoids rapid switching, and whether the group selection survives a core reload. Once those tests pass, schedule the script at a conservative interval and review its logs periodically instead of treating automation as a substitute for monitoring.
With a protected controller, a stable strategy group, encoded API requests, realistic health checks, and hysteresis around switching decisions, Clash becomes a manageable failover component rather than a collection of manual node buttons. The API supplies the control surface; the quality of the result depends on how carefully the script defines “healthy,” how much change it allows, and how clearly it records each decision.
DOWNLINK READY
Download the Clash Client
Download links for Clash clients on Windows, macOS, Linux, Android, and iOS, including builds with the mihomo core and GUI.