Clash proxy groups can already perform basic automatic selection with url-test and fallback. However, built-in group behavior is not always enough for advanced environments. You may want to test several endpoints, apply different rules for work and personal traffic, record latency over time, or switch a group only after repeated failures. Clash's external controller makes this possible through a REST API. A small shell, Python, or PowerShell script can read the current proxy state, test individual nodes, and select a healthier route without editing the YAML file or restarting Clash.

This guide focuses on practical controller automation for Clash and Mihomo-compatible clients. It explains how the API is exposed, how proxy groups are represented, how to perform health checks, how to avoid unstable switching, and how to secure the controller when it is accessed from another device. The examples use standard HTTP requests so they can be adapted to Clash Verge, Clash Verge Rev, Clash for Windows alternatives, Clash for Android, routers, and headless Mihomo installations.

Understanding API-Based Node Switching

There are two different layers involved in automatic node selection. The first layer is Clash's internal proxy-group engine. A url-test group periodically measures latency and chooses the fastest member, while a fallback group follows a priority order and moves to the next node when the current one becomes unavailable. This approach is simple, reliable, and usually the best starting point.

The second layer is external automation. A script talks to the external controller and makes decisions based on information that the built-in group cannot easily use. For example, a script can:

  • Test a node against several URLs instead of relying on one endpoint.
  • Prefer a region or provider when latency is similar.
  • Require two or three consecutive failures before switching.
  • Avoid a node that responds quickly but cannot reach the services you actually use.
  • Switch different proxy groups independently, such as Streaming, Work, and Proxy.
  • Write a log containing latency, failure counts, selected nodes, and switch reasons.

The external controller normally listens on an address such as 127.0.0.1:9090. API requests use HTTP methods to read or modify state. A simplified workflow looks like this:

  1. Read the available proxy groups from /proxies.
  2. Find the target group and its member nodes.
  3. Send delay tests to selected nodes using /proxies/{name}/delay.
  4. Compare successful results with a policy such as maximum latency or preferred region.
  5. Update the group with PUT /proxies/{group} only when a switch is justified.
  6. Verify the selected node and record the result.
Use built-in groups first: API scripting is most useful when you need custom health logic, multiple test targets, scheduling, notifications, or cross-group coordination. For ordinary fastest-node selection, a well-tuned url-test group is easier to maintain.

Enabling and Securing the External Controller

Add an external controller and a secret to the active Clash configuration. The exact location of the configuration editor differs between clients, but the YAML fields are the same in Clash-compatible cores.

External Controller Configuration
# Keep the API local unless remote management is required
external-controller: 127.0.0.1:9090
secret: replace-with-a-long-random-token

Binding the controller to 127.0.0.1 means that only applications on the same machine can connect. This is the safest default for a desktop client. A local script can call the API without exposing the control plane to your Wi-Fi network, office network, or the public Internet.

If the Clash core runs on a router or another server and you need to manage it from a different device, bind the controller to a specific LAN address or to 0.0.0.0:9090. The latter listens on every network interface and should be used only with a strong secret and a firewall rule that limits source addresses. Do not expose an unauthenticated controller port to the Internet. The API can change routes, inspect connection metadata, and in some builds modify runtime configuration, so it must be treated like an administrative service.

Most requests use a Bearer token in the Authorization header. A basic connectivity test looks like this:

Check the Controller
curl -H "Authorization: Bearer replace-with-a-long-random-token" \
  http://127.0.0.1:9090/version

If authentication is accepted, the response normally contains core version information. A connection refusal means the controller is disabled, the address or port is incorrect, or a firewall is blocking access. A 401 response usually indicates an incorrect or missing secret. On some clients, the graphical application may overwrite manual configuration changes, so confirm that the controller fields remain present after restarting the client.

Never place the secret in a public script repository: Use an environment variable, a protected configuration file, or an operating-system secret store. Shell history, process listings, terminal recordings, and CI logs can accidentally reveal tokens.

Reading Proxy Groups and Testing Nodes

The /proxies endpoint returns the runtime proxy map. It includes individual nodes, proxy groups, and special entries such as DIRECT and REJECT. A group usually has a type, a list of all members, and a current now selection.

List Proxies and Groups
curl -s \
  -H "Authorization: Bearer $CLASH_SECRET" \
  http://127.0.0.1:9090/proxies

For a script, do not assume that every entry in all is a real server. A group may contain another group, a selector, or a built-in route. Before sending delay tests, filter out values such as DIRECT, REJECT, and nested groups unless your policy explicitly supports them.

To test one node, URL-encode the node name and call the delay endpoint. Names containing spaces, slashes, symbols, or non-ASCII characters must be encoded correctly. The test URL should return a fast, predictable response. An HTTP 204 endpoint is often preferable to a large web page because the result focuses on connectivity rather than download speed.

Test One Node
curl -G \
  -H "Authorization: Bearer $CLASH_SECRET" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000" \
  "http://127.0.0.1:9090/proxies/Node%20JP%2001/delay"

A successful response commonly contains a delay value in milliseconds. A timeout, DNS failure, TLS failure, or connection error should be treated as an unsuccessful test rather than as a zero-latency result. Different core versions and clients may report errors with slightly different JSON structures, so robust code should check the HTTP status and validate that the returned delay is a positive number.

One endpoint is not a complete definition of node health. A node can reach a Google test URL but fail to connect to a work service, a video platform, or an API that matters to you. For better decisions, test two or three small endpoints and use a rule such as “at least two successful checks” or “the median delay must be below 800 ms.” Avoid testing too many URLs on every cycle because each test consumes bandwidth and may trigger rate limits.

Switching a Proxy Group Through the API

Once the script has chosen a candidate, change the selected member of the group with a PUT request. The group name is part of the URL and the selected proxy name is sent as a JSON field. Both values should be encoded safely.

Switch the Active Group
curl -X PUT \
  -H "Authorization: Bearer $CLASH_SECRET" \
  -H "Content-Type: application/json" \
  --data '{"name":"Node JP 01"}' \
  "http://127.0.0.1:9090/proxies/Proxy"

Replace Proxy with the exact group name shown by /proxies. The request normally returns an empty success response, so verification is important. Read the group again and confirm that its now field equals the intended node. If it does not, the node may not belong to that group, the name may be incorrectly encoded, or another automation process may have changed the selection at the same time.

A compact Python implementation can perform the same operation while keeping the selection policy easy to adjust:

Python Node Selection Example
import os
import statistics
import time
import requests

BASE = os.getenv("CLASH_API", "http://127.0.0.1:9090")
TOKEN = os.environ["CLASH_SECRET"]
GROUP = "Proxy"
TEST_URL = "https://www.gstatic.com/generate_204"
CANDIDATES = ["Node JP 01", "Node SG 01", "Node US 01"]

HEADERS = {"Authorization": f"Bearer {TOKEN}"}

def check_node(name):
    response = requests.get(
        f"{BASE}/proxies/{requests.utils.quote(name, safe='')}/delay",
        params={"url": TEST_URL, "timeout": 5000},
        headers=HEADERS,
        timeout=8,
    )
    response.raise_for_status()
    value = response.json().get("delay")
    return value if isinstance(value, int) and value > 0 else None

def switch_to(name):
    response = requests.put(
        f"{BASE}/proxies/{requests.utils.quote(GROUP, safe='')}",
        json={"name": name},
        headers=HEADERS,
        timeout=5,
    )
    response.raise_for_status()

results = {}
for node in CANDIDATES:
    try:
        results[node] = check_node(node)
    except requests.RequestException:
        results[node] = None

healthy = [(delay, node) for node, delay in results.items() if delay]
if healthy:
    healthy.sort()
    best_delay, best_node = healthy[0]
    current = requests.get(
        f"{BASE}/proxies/{requests.utils.quote(GROUP, safe='')}",
        headers=HEADERS,
        timeout=5,
    ).json().get("now")

    if current != best_node and (current not in results or
                                  results.get(current) is None or
                                  best_delay + 80 < results[current]):
        switch_to(best_node)
        print(f"Switched to {best_node}: {best_delay} ms")
    else:
        print(f"Kept {current}")
else:
    print("No healthy candidate was found")

The example uses an 80 ms improvement threshold. This is deliberate: selecting the absolute fastest result on every cycle can cause route flapping when two nodes differ by only a few milliseconds. The script also keeps the current node when it remains healthy and reasonably close to the best candidate.

Building a Reliable Failover Policy

A useful failover script needs more than a single comparison. Internet latency changes constantly because of congestion, Wi-Fi interference, overloaded servers, and temporary DNS problems. If one failed request immediately triggers a switch, the controller may rotate through every node and make the connection less stable than before.

Use the following policy principles:

  • Use consecutive failures: switch only after two or three failed cycles, not after one timeout.
  • Add a cooldown: after switching, wait several minutes before selecting another node unless the new node also fails completely.
  • Separate failure from slowness: a 1,200 ms response may still be usable, while a connection timeout is a hard failure.
  • Keep a hysteresis threshold: require the candidate to be meaningfully faster, such as 50–150 ms, before changing.
  • Preserve a last-known-good node: if every test fails, keep the current selection instead of switching randomly.
  • Test the actual group: a node can exist in the configuration but not be a member of the group you intend to update.

For scheduled execution, run the script every few minutes with Task Scheduler, cron, or a systemd timer. Avoid launching overlapping copies. Two concurrent processes can test different states and then overwrite each other's selections. A lock file or a short-lived operating-system mutex prevents this race.

It is also useful to distinguish between node testing and route testing. A delay request tests whether Clash can use a particular proxy for one URL, but it does not prove that every rule selects that proxy. If a domain is matched by a direct rule, switching the Proxy group will not change that domain's route. When troubleshooting, inspect active connections and rules as well as the group selection.

Simple Cron Schedule
# Run every five minutes; redirect output to a protected log
*/5 * * * * /usr/local/bin/clash-switch.py >> /var/log/clash-switch.log 2>&1

Logs should include the timestamp, group name, tested nodes, measured delays, selected node, and reason for switching. Do not log the API secret, full authorization header, private URLs, or sensitive connection metadata. A concise log makes it much easier to determine whether a switch was caused by a genuine outage or by an overly aggressive threshold.

Choosing Between YAML Groups and API Scripts

API automation is powerful, but it should not replace configuration features that already solve the problem cleanly. Use url-test when the goal is simply to choose the lowest-latency node at a fixed interval. Use fallback when you have a clear primary and backup order. These groups continue working even when your external script is stopped.

Built-In Groups for Basic Failover
proxy-groups:
  - name: Fastest
    type: url-test
    proxies:
      - Node JP 01
      - Node SG 01
      - Node US 01
    url: https://www.gstatic.com/generate_204
    interval: 300
    tolerance: 80

  - name: Primary Backup
    type: fallback
    proxies:
      - Node JP 01
      - Node SG 01
      - Node US 01
    url: https://www.gstatic.com/generate_204
    interval: 300

Use an API script when the decision depends on business rules, service-specific tests, historical measurements, time of day, or external notifications. A practical hybrid design keeps a stable fallback group in YAML and lets the API select among a smaller set of policy groups. That way, the core still has a basic recovery path if the custom automation stops.

Before deploying a script permanently, test it manually with a non-critical group. Confirm the controller address, secret handling, URL encoding, error behavior, and switch threshold. Then run it in report-only mode for a day so you can observe what it would have selected without changing live traffic. After that, enable switching and monitor the logs for unnecessary flapping.

With a protected external controller, accurate health checks, and conservative switching rules, Clash can become more than a static proxy selector. It can continuously adapt to changing network conditions while keeping the decision process visible and reproducible. Start with the built-in groups, add API control only where it provides a clear advantage, and keep the fallback path simple enough to recover when the network or the automation itself fails.

Get Started

Take Full Control of Your Traffic with Clash

Available on Windows, macOS, Linux, Android, and iOS. Flexible rules, simple setup, ready to use.

Download Free View Setup Guide →