Clash API Auto-Switching: Advanced Node Failover Guide 2026

Manual proxy changes are unreliable when a node times out during builds, AI sessions, or remote operations. This guide designs an automated failover workflow around the Clash API, combining health checks, latency thresholds, strategy-group control, secure authentication, and production-ready service deployment.

Why Automatic Failover Matters in Clash

A manually selected proxy node can work perfectly at breakfast and become unusable during a long build, an AI session, or a remote maintenance task. The failure may be a connection timeout, a stalled TLS handshake, packet loss, or a node that still accepts connections but responds too slowly to be practical. Clicking a different node in the client solves the immediate problem, but it does not create a repeatable recovery process.

Clash and mihomo expose a controller API that can be used to inspect proxy groups, measure node latency, and change the selected proxy in a strategy group. A small external service can therefore monitor a group and switch to a healthier node when the active one fails. The important distinction is that the script should not rewrite the entire configuration every time. It should call the running Clash controller and change only the group selection that needs attention.

A reliable failover design has five separate parts:

  • Health checks: test a node against a URL that represents the traffic you actually care about.
  • Latency policy: define acceptable response time instead of treating every successful response as equally good.
  • Strategy-group control: switch the group through the controller API rather than editing YAML while Clash is running.
  • Authentication and network boundaries: protect the external controller because it can change routing for every application on the device.
  • Operational safeguards: use cooldowns, failure counters, and recovery rules so a temporary network fluctuation does not cause constant node switching.

Failover is not load balancing

Failover keeps one preferred node active and moves to another node only when the policy decides that the current path is unhealthy. Load balancing distributes requests across several nodes. Do not use a failover script when your real goal is concurrent traffic distribution; the two designs have different failure and session characteristics.

The Clash Controller API and Proxy-Group Model

The external controller is normally configured with external-controller, for example 127.0.0.1:9090. A controller bound to the loopback address is reachable only from the local machine, which is the safest default for a local automation script. If the script runs on another host, the controller must listen on an address reachable from that host, but it should then be restricted by a firewall or private management network.

Most API requests use the controller base URL followed by a REST path. The endpoints needed for node failover are:

  • GET /proxies returns the available proxy nodes and proxy groups.
  • GET /proxies/<group-name> returns details for a specific group, including its current selection and available members.
  • GET /proxies/<node-name>/delay?url=...&timeout=... measures a node against a test URL.
  • PUT /proxies/<group-name> changes the selected member of a selectable strategy group. The JSON body is typically {"name":"Node Name"}.

Names are part of the URL, so they must be URL-encoded. A node name containing spaces, slashes, or non-ASCII characters should never be concatenated into a path manually. Use a URL builder such as Python's quote function. Also remember that the controller API is exposed by the running client and kernel; the exact supported endpoints can vary between old Clash builds and current mihomo-based clients. Test the endpoint manually before putting it into a service.

The API can control a group only when the group already exists in the loaded configuration. A basic group can be declared with a stable name and a list of node names. The following example is intentionally generic: replace the node names with entries that actually exist in your subscription.

external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-secret"

proxy-groups:
  - name: AUTO-FAILOVER
    type: fallback
    url: "https://www.example.com/generate_204"
    interval: 300
    tolerance: 100
    proxies:
      - "Node-A"
      - "Node-B"
      - "Node-C"

rules:
  - MATCH,AUTO-FAILOVER

The built-in fallback group already performs health-based selection inside the Clash kernel. It is often the right answer when the requirement is simply “use the first healthy node.” An external script becomes useful when the decision needs extra logic: several test URLs, a custom latency ceiling, maintenance exclusions, notification hooks, a business-hours preference, or coordination with another service.

Do not confuse a fallback group with a select group. A select group is manually controlled and is usually the clearest target for an external script. A fallback group may also be queried and controlled, but its own health-check cycle can compete with the script if both are trying to select different members. Pick one authority for selection whenever possible.

Designing Health Checks That Reflect Real Traffic

A node that answers a TCP connection is not necessarily suitable for your workload. A health check should test the complete path far enough to expose the failure that matters: DNS resolution, proxy negotiation, TLS setup, and an HTTP response. The test URL should be small, stable, and expected to respond quickly. Avoid a large download page, an endpoint that changes content frequently, or a service that rate-limits repeated probes.

Latency thresholds must be explicit. For example, a script might classify a result below 800 milliseconds as healthy, 800 to 1,500 milliseconds as degraded, and a timeout or higher result as failed. These numbers are not universal. An interactive terminal session may need a lower threshold than a background package download, while a long-lived video connection may care more about packet loss than the first response time.

Result Suggested interpretation Typical action
HTTP response and low delay Healthy path Keep the node eligible
HTTP response above the threshold Degraded path Prefer another eligible node if available
Timeout or controller error Failed check Increment the failure counter
One failed probe Possibly transient Do not switch immediately
Several consecutive failures Persistent failure Switch and start a cooldown

Use consecutive failures and consecutive successes rather than a single result. A practical starting policy is three failed checks before switching, a 30-second minimum interval between switches, and two successful checks before restoring a preferred node. This prevents oscillation when two nodes are close to the threshold or when the local network briefly loses packets.

Health checks also need an eligibility list. The group may contain a provider placeholder, a nested group, or a node reserved for a special purpose. The script should inspect the group returned by the API and ignore members that are not real selectable nodes. It should never assume that the first item in the list is a usable proxy.

Choosing test URLs and timeouts

Choose a test URL that is reachable through the same route as the important traffic. If your users connect to an internal service, a public endpoint alone is not enough. Conversely, if the script runs before the full application starts, an internal endpoint may create a false failure. Keep the timeout shorter than the monitoring interval; a 5-second probe executed every 10 seconds can consume most of the control loop when several nodes are tested serially.

Testing every node on every cycle is simple but expensive. A more conservative design checks the active node frequently and checks alternatives only after a failure or on a slower discovery interval. Parallel checks reduce total waiting time, but they create more simultaneous connections and can trigger provider-side limits. Start with sequential checks, measure the behavior, and add concurrency only when the node count requires it.

A Practical Python Failover Script

The following example uses Python's standard library, so it does not require an additional package. It reads the controller secret from an environment variable, checks the current group, measures candidate nodes, and changes the selection only after the active node has failed repeatedly. The endpoint names and JSON shape follow the common Clash and mihomo controller API; verify them against the client version you operate.

import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

CONTROLLER = os.getenv("CLASH_CONTROLLER", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "AUTO-FAILOVER")
TEST_URL = os.getenv("CLASH_TEST_URL", "https://www.example.com/generate_204")
TIMEOUT_MS = int(os.getenv("CLASH_MAX_DELAY_MS", "1200"))
PROBE_TIMEOUT_MS = int(os.getenv("CLASH_PROBE_TIMEOUT_MS", "5000"))
FAIL_LIMIT = int(os.getenv("CLASH_FAIL_LIMIT", "3"))
COOLDOWN = int(os.getenv("CLASH_SWITCH_COOLDOWN", "30"))

failures = 0
last_switch = 0.0

def api(method, path, payload=None):
    body = None if payload is None else json.dumps(payload).encode()
    request = Request(
        CONTROLLER + path,
        data=body,
        method=method,
        headers={
            "Authorization": "Bearer " + SECRET,
            "Content-Type": "application/json",
        },
    )
    with urlopen(request, timeout=10) as response:
        raw = response.read()
        return json.loads(raw) if raw else {}

def group_state():
    return api("GET", "/proxies/" + quote(GROUP, safe=""))

def node_delay(node):
    query = urlencode({
        "url": TEST_URL,
        "timeout": str(PROBE_TIMEOUT_MS),
    })
    path = "/proxies/" + quote(node, safe="") + "/delay?" + query
    result = api("GET", path)
    return int(result["delay"])

while True:
    try:
        state = group_state()
        current = state.get("now")
        members = state.get("all", [])
        if not current or not members:
            raise RuntimeError("group has no current member or candidates")

        try:
            delay = node_delay(current)
            current_ok = delay <= TIMEOUT_MS
        except (HTTPError, URLError, KeyError, ValueError):
            current_ok = False

        if current_ok:
            failures = 0
        else:
            failures += 1

        if failures >= FAIL_LIMIT and time.time() - last_switch >= COOLDOWN:
            ranked = []
            for node in members:
                if node == current:
                    continue
                try:
                    delay = node_delay(node)
                    if delay <= TIMEOUT_MS:
                        ranked.append((delay, node))
                except (HTTPError, URLError, KeyError, ValueError):
                    continue

            if ranked:
                ranked.sort()
                replacement = ranked[0][1]
                api("PUT", "/proxies/" + quote(GROUP, safe=""),
                    {"name": replacement})
                print("switched", current, "to", replacement)
                failures = 0
                last_switch = time.time()

    except (HTTPError, URLError, RuntimeError, KeyError, ValueError) as error:
        print("monitoring error:", error)

    time.sleep(10)

This script intentionally avoids changing the group after one bad result. It also measures alternatives only when the active node fails, which keeps normal API traffic low. The fastest candidate is selected from the nodes that satisfy the threshold. That is not always the best production policy: you may prefer a fixed priority order, a node in a specific region, or a node that has passed several consecutive probes.

Do not expose the secret in the command line

Command-line arguments can appear in process listings and shell history. Store the controller secret in a protected environment file, operating-system credential store, or service manager environment. Restrict the file permissions and rotate the secret if it appears in logs, a screenshot, or a shared configuration.

Secure Deployment and Service Operations

The controller API is a privileged interface. Anyone who can call its proxy-selection endpoint may redirect traffic, disable an intended route, or inspect runtime information. Keep external-controller on 127.0.0.1 when the monitor runs on the same machine. If remote administration is required, bind it to a private management address, apply firewall rules, and avoid forwarding the controller port directly to the public internet.

Use the API secret consistently. Clash clients commonly send it as a bearer token in the Authorization header. Do not put the token in a URL, a browser bookmark, or a copied curl command that will be stored in shell history. A local monitor should also run under a dedicated operating-system account with only the permissions needed to read its environment and write its own logs.

Running the monitor as a managed service

For a desktop machine, launching the script manually may be enough while testing. For a build server or remote workstation, use the operating system's service manager so the process starts after Clash is available, restarts after an unexpected exit, and writes bounded logs.

With systemd, a minimal unit could look like this:

[Unit]
Description=Clash API failover monitor
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=clash-monitor
EnvironmentFile=/etc/clash-failover.env
ExecStart=/usr/bin/python3 /opt/clash/failover.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

The environment file should contain values such as CLASH_SECRET, CLASH_GROUP, and CLASH_TEST_URL, with permissions that prevent ordinary users from reading it. Start the service only after confirming that Clash's controller is listening. A restart loop caused by a wrong group name or a missing secret is not failover; it is an unmonitored system with extra log noise.

On Windows, the same principles apply when using Task Scheduler or a service wrapper: delay startup until the Clash client has created its controller listener, run the task with the least privilege required, and configure a restart action. On macOS, a launch agent can start the monitor for the logged-in user, but the script still needs a clear dependency on the client and must not assume that the controller is ready immediately after login.

Testing, Observability, and Failure Recovery

Test the API before testing failover. First confirm that the controller responds locally, then query the group, then measure one known node, and finally perform a controlled selection change. A safe test sequence is:

  1. Confirm the controller address and secret are correct without printing the secret in the terminal.
  2. Request the target group and verify that its name, current member, and candidate list are what the script expects.
  3. Measure a healthy node with a short test URL and record the returned delay value.
  4. Temporarily use an invalid or blocked test URL in a test environment to confirm that failure counters increase.
  5. Switch to a known backup node and verify an actual application connection, not just the API response.
  6. Restore the preferred node and confirm that cooldown and recovery rules prevent immediate oscillation.

Keep logs useful and small. Record timestamps, group name, previous node, replacement node, measured delay, failure count, and the reason for a switch. Never record subscription URLs, API secrets, full authorization headers, or complete configuration files. If an application has long-lived connections, remember that changing the group affects new connections; existing TCP sessions may remain attached to the old path until they close or time out.

Symptom Likely cause Check
Connection refused Controller is disabled or listening elsewhere Check external-controller and the client listener
401 or 403 response Missing or incorrect bearer secret Inspect the environment variable and request header
404 for a node path Node name was not URL-encoded or is no longer in the config Read the group from /proxies/<group> first
Every node times out Test URL is unreachable or the client is not routing the probe as expected Try another stable URL and inspect Clash logs
Constant switching Threshold is too strict or there is no cooldown Increase failure limits and compare several probe results
API says switched but traffic is unchanged Traffic uses another group, a cached connection, or a different mode Check rules, active mode, and whether new connections use the group

For production use, add a maximum switch rate and an all-nodes-failed state. If no candidate passes the check, keep the current selection or switch to an explicitly defined emergency group rather than choosing a random node. Send an alert only after a sustained outage; notifying on every failed probe makes real incidents harder to notice.

Finally, compare this external workflow with the built-in fallback group before maintaining custom code. The built-in mechanism is easier to upgrade and has fewer moving parts. An API monitor is justified when its additional policy is valuable and observable. Start with a narrow group, a conservative threshold, and a local controller, then expand the automation after controlled tests show that it improves recovery instead of creating route instability.

Next Steps: Install, Configure, Then Automate

Use a maintained Clash or mihomo-based client, load the configuration, and confirm ordinary proxy switching before introducing an automated monitor. Once the group behaves correctly by hand, the API script can take over the repetitive checks without hiding the underlying routing logic.

Download the Clash Client

Rule-based routing needs a client to take over traffic first. Head to the download hub, pick a client for your platform, then come back to this guide to finish setting up system proxy or TUN takeover.

Download Clash