Clash API Node Switching: Advanced Automation Guide

What Clash API Automation It makes sense to Actually Do in Depth

Most of of of Clash users begin with a one at a time selected proxy group: open the client, click a node, wait for a page to load, and repeat the process when the connection becomes slow.

The Clash external controller API gives you a way to turn that manual process into a small failover system. A script can ask Mihomo or another Clash-compatible core which proxies exist, test selected nodes through the controller, compare their measured lag, and move a proxy group to a working node.

The important distinction is that the script does not will want to rewrite the entire YAML profile. It can operate on the running configuration through authenticated HTTP requests, leaving rules, DNS settings, ports, and policy groups intact.

Mechanically:This guide focuses on a practical Clash API node switching design reasonably than a theoretical monitoring platform. You will learn how to expose the controller safely, discover proxy names, test lag with the correct URL, re-sync a selector group, and add safeguards so a temporary timeout does not cause constant switching.

The examples use Shell and Python because they are available on most of of of Windows, macOS, and Linux systems. The same API concepts also apply when the controller is provided by Clash Verge Rev, Mihomo Party, Clash for Android, or another Mihomo-based client.

That approach is acceptable for occasional browsing, but it becomes fragile when Clash is used for development tools, scheduled jobs, remote administration, API clients, or services that must remain reachable for hours. A node can remain present in the subscription while its TLS handshake fails, its route becomes congested, or its lag changes sharply during the day.

Core compatibility matters: endpoint names and response fields can differ slightly between legacy Clash, Clash Meta, and Mihomo releases.

Test the API one at a time first, and treat the response from your installed core as the source of truth reasonably than copying an old screenshot or undocumented example.

Prepare and Secure the External Controller Mechanically

The controller is an HTTP front-end for the running proxy core. It commonly exposes endpoints for reading the current configuration, listing proxies, measuring delay, and changing the active member of a proxy group.

More strictly:Do not bind the controller to every network front-end unless you have a specific administrative reason and a firewall policy that limits access. A controller exposed on 0.0.0.0 can become a remote control panel for your proxy process. Anyone who can reach it may be able to select nodes, inspect connection information, or change runtime behaviour.

For a local automation script, binding to 127.0.0.1 is normally sufficient. If automation operates on another machine, place the controller behind a private network, firewall rule, or authenticated tunnel instead of publishing it with no detour to the internet.

Use the Authorization header for API requests when a secret is configured. In plenty Mihomo-compatible implementations the format is Bearer YOUR_SECRET. Keep the secret out of shell history, public repositories, screenshots, and process arguments when possible.

At a different level:A local environment variable or a file readable only by your user account is safer than embedding the value in every script. On Unix-like systems, a credentials file it makes sense to normally use permissions such as chmod 600.

A typical local address is 127.0.0.1:9090, although the port may be different in your client. The controller secret is more often than not configured as secret in the external-controller section of a YAML profile or in the client’s advanced settings.

Test the controller before writing automation Internals

Kick off with a harmless read request. Replace the port and secret with the values from your own client:

export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="replace-with-your-secret"

curl --fail --silent \
  -H "Authorization: Bearer ${CLASH_SECRET}" \
  "${CLASH_API}/proxies"

A successful response with unexpected proxy names is not an exception: subscription providers often include nested groups, load-balancing groups, fallback groups, and policy groups alongside individual nodes.

Thinking it through:Before changing anything, identify the group that your rules actually use. For example, a rule may reference PROXY, while another profile may use 🚀 Proxy, Auto, or a provider-specific name.

Switching a group that is not referenced by whichever you pick active rule will produce a technically successful API response but no visible traffic change. Read the active configuration or inspect the client’s proxy-group panel to confirm the exact group name, including spaces, emoji, punctuation, and capitalization.

A successful response is a JSON object containing proxy definitions and, in most of of of versions, a special GLOBAL or DIRECT entry. If you receive 401 Unauthorized, check the secret and header format. A connection refusal more often than not means that the core is not running, the port is wrong, or the client uses a different controller address.

Choose a Useful Node Health Test Step by Step

A lag test is not the same as a everything included guarantee that a node will work for every destination. The controller more often than not measures a proxy by requesting a URL through that proxy and reporting the elapsed time.

First, a clarification:Choose a not jumpy HTTPS endpoint that returns quickly and does not require login. Plenty Clash-compatible cores accept a request shaped like this:

The proxy name must be URL-encoded. A name such as US Premium 01 cannot safely be inserted into a path with literal spaces. The API may return a JSON object containing a delay value in milliseconds, or an exception when the request times out.

curl --fail --silent \
  -H "Authorization: Bearer ${CLASH_SECRET}" \
  --get "${CLASH_API}/proxies/Example%20Node/delay" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000"

Certain cores require a different test URL or expose a slightly different delay endpoint, so confirm the route in your installed version if this request returns a 404.

Mechanically:Use a timeout that reflects the job. A two-second test is useful for interactive browsing but may reject a node during a brief congested period. A ten-second test lowers false negatives but makes failover slower. For a personal workstation, five seconds is a reasonable starting point.

You may also perform two tests several hundred milliseconds apart and accept a node only when both succeed. That costs more requests, but it prevents one transient packet loss event from changing the active route.

Lag alone it makes sense to not be the only selection rule. A good candidate filter may exclude groups, DIRECT, REJECT, expired provider entries, and nodes whose names match words such as “ทดลอง” or “backup” when your provider uses those labels for special-purpose entries.

More strictly:If the provider exposes a dedicated group of not jumpy nodes, prefer that group instead of scanning every object returned by /proxies.

This is useful for detecting dead transports, broken TLS, and obviously congested nodes, but it does not prove that every website, API, or streaming service is reachable. A node may be fast against a small test endpoint and still be unsuitable for a particular destination because of routing restrictions or remote-server policy.

Do not test too aggressively. A script that checks dozens of nodes every few seconds can create unnecessary traffic, trigger provider limits, and make the controller appear unstable.

Kick off with a five- or ten-minute interval, test only a small candidate set, and add a cooldown after every successful switch.

Build a Safe Automatic Switching Workflow Internals and Practice

The most of of of it holds up design separates discovery, testing, decision-making, and switching. Discovery reads the current proxy list. Testing measures only eligible leaf nodes.

Decision-making compares successful results with the current node and applies a meaningful improvement threshold. Switching re-syncs one known group. Finally, verification checks that the group really changed and records the result in a log.

  1. Read the active proxy list: request /proxies and parse the JSON response instead of maintaining a second hard-coded list that becomes stale after every subscription re-sync.
  2. Identify the target group: choose the exact group used by your rules, then read its current selected member before making a change.
  3. Filter candidates: ignore DIRECT, REJECT, nested groups, and whichever you pick node that your local naming policy marks as unsuitable.
  4. Measure candidates: call the delay endpoint with a bounded timeout and store both successful lag and failure details.
  5. Apply a decision threshold: switch only when the current node fails or the best candidate is faster by a configured margin, such as 20 percent or 150 milliseconds.
  6. Re-sync the group: send a URL-encoded request to the group’s selection endpoint, then wait briefly for the core to apply the change.
  7. Verify and cool down: read the group again, write a timestamped log entry, and prevent another switch until the cooldown expires.

A compact Shell implementation Mechanics

The following script demonstrates the core pattern. It assumes that your controller supports the common GET /proxies, GET /proxies/{name}/delay, and PUT /proxies/{group} endpoints.

At a different level:This example intentionally stays conservative: it switches when the best candidate is available, but a production version it makes sense to compare the best result with the current node’s fresh delay.

#!/usr/bin/env bash
set -euo pipefail

API="${CLASH_API:-http://127.0.0.1:9090}"
SECRET="${CLASH_SECRET:?Set CLASH_SECRET first}"
GROUP="${CLASH_GROUP:-PROXY}"
TEST_URL="${CLASH_TEST_URL:-https://www.gstatic.com/generate_204}"
TIMEOUT="${CLASH_TIMEOUT:-5000}"
MIN_GAIN="${CLASH_MIN_GAIN:-150}"

auth=(-H "Authorization: Bearer ${SECRET}")

current="$(curl --fail --silent "${auth[@]}" \
  "${API}/proxies/${GROUP// /%20}" | jq -r '.now')"

mapfile -t candidates < <(curl --fail --silent "${auth[@]}" \
  "${API}/proxies" |
  jq -r --arg group "$GROUP" '
    .proxies[$group].all[]?
    | select(. != "DIRECT" and . != "REJECT")
  ')

best_name=""
best_delay=999999

for name in "${candidates[@]}"; do
  encoded="$(jq -rn --arg v "$name" '$v|@uri')"
  result="$(curl --silent --show-error \
    --max-time 8 "${auth[@]}" --get \
    "${API}/proxies/${encoded}/delay" \
    --data-urlencode "url=${TEST_URL}" \
    --data-urlencode "timeout=${TIMEOUT}" || true)"
  delay="$(jq -r '.delay // empty' <<< "$result")"

  if [[ "$delay" =~ ^[0-9]+$ ]] && (( delay < best_delay )); then
    best_name="$name"
    best_delay="$delay"
  fi
done

if [[ -z "$best_name" ]]; then
  echo "No healthy candidate found" >&2
  exit 1
fi

if [[ "$best_name" == "$current" ]]; then
  echo "Keep ${current}; best delay ${best_delay} ms"
  exit 0
fi

curl --fail --silent --request PUT "${auth[@]}" \
  --header "Content-Type: application/json" \
  --data "$(jq -n --arg name "$best_name" '{name:$name}')" \
  "${API}/proxies/${GROUP// /%20}" > /dev/null

echo "Switched ${GROUP}: ${current} -> ${best_name} (${best_delay} ms)"

Otherwise, an improvement of one millisecond can cause needless movement. Add a current-node test, a minimum improvement threshold, and a failure counter before using the script for a long-running service.

Adjust the group name and test URL, and install jq if your system does not already provide it.

Python automation with hysteresis by Step

Python is useful when you want clearer exception handling, persistent state, or a more sophisticated policy. The following example uses the standard library only.

Thinking it through:The use of hysteresis is important. Without it, two nodes with nearly identical lag can alternate on every polling cycle. The MIN_GAIN_MS value creates a buffer: the new node must be substantially better before it replaces the current one.

import json
import os
import time
import urllib.parse
import urllib.request

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "PROXY")
TEST_URL = os.getenv("CLASH_TEST_URL", "https://www.gstatic.com/generate_204")
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT", "5000"))
MIN_GAIN_MS = int(os.getenv("CLASH_MIN_GAIN", "150"))

headers = {"Authorization": f"Bearer {SECRET}"}

def request(path, method="GET", payload=None):
    body = None
    local_headers = dict(headers)
    if payload is not None:
        body = json.dumps(payload).encode()
        local_headers["Content-Type"] = "application/json"
    req = urllib.request.Request(
        API + path, data=body, headers=local_headers, method=method
    )
    with urllib.request.urlopen(req, timeout=8) as response:
        return json.loads(response.read().decode())

def quote_name(name):
    return urllib.parse.quote(name, safe="")

def delay_for(name):
    path = f"/proxies/{quote_name(name)}/delay"
    query = urllib.parse.urlencode({"url": TEST_URL, "timeout": TIMEOUT_MS})
    try:
        data = request(f"{path}?{query}")
        return int(data["delay"])
    except Exception:
        return None

data = request("/proxies")
group = data["proxies"][GROUP]
current = group.get("now")
candidates = [
    name for name in group.get("all", [])
    if name not in {"DIRECT", "REJECT"} and name != GROUP
]

results = {name: delay_for(name) for name in candidates}
healthy = {name: delay for name, delay in results.items()
           if delay is not None}

if not healthy:
    raise SystemExit("No healthy proxy candidate")

best = min(healthy, key=healthy.get)
current_delay = delay_for(current) if current else None

if current_delay is not None and healthy[best] + MIN_GAIN_MS >= current_delay:
    print(f"Keep {current}: current={current_delay} ms, best={healthy[best]} ms")
else:
    request(f"/proxies/{quote_name(GROUP)}", "PUT", {"name": best})
    time.sleep(0.5)
    confirmed = request(f"/proxies/{quote_name(GROUP)}").get("now")
    print(f"Selected {confirmed}; delay={healthy[best]} ms")

You may also require three consecutive failures before abandoning the current node, while allowing one successful test to restore it later.

It tests a supplied list of node names, keeps the current node when it is still healthy, and requires the new node to be meaningfully better before switching.

Guardrails for Long-Running Automation in Depth

A node switch can interrupt active downloads, WebSocket sessions, remote shells, and API requests. For that reason, the automation it makes sense to be aware of what it is controlling. If the group carries a long-lived connection, prefer switching only after a confirmed failure instead of optimising every small lag difference.

First, a clarification:Subscription re-syncs can rename or remove nodes. A script it makes sense to handle a missing group and an empty candidate list without replacing the active profile or writing a broken configuration. Log the controller status, target group, current selection, candidate count, selected lag, and reason for every decision.

Avoid logging full subscription URLs because they may contain credentials. When a script fails, the log it makes sense to tell you whether the failure was sign-in, JSON parsing, timeout, missing node, or an API compatibility issue.

Use a lock: prevent two scheduled jobs from testing and switching the same group simultaneously.; Set a cooldown: wait several minutes after a switch before allowing another one.; Limit concurrency: test a small number of nodes at a time so the client and provider are not flooded.; Keep a fallback: define a known-good node or a manual group for recovery when every automated candidate fails.; Verify the destination: lag to the test URL it makes sense to be followed by a real request to the service that matters.; Respect privacy: do not expose controller responses, proxy names, or secrets in public monitoring dashboards..

Mechanically:For scheduling, Task Scheduler on Windows, launchd on macOS, and systemd timers or cron on Linux can operate the checker. A timer interval is generally preferable to a permanently running loop because it is easier to inspect and restart.

Whatever scheduler you use, make sure the environment variables containing the controller URL and secret are available to the job. Scheduled processes often operate with a different working directory and a reduced environment, which explains plenty “works in my terminal” reports.

If the group is used for short browser requests, a faster-node policy may be acceptable. Separate groups are often the cleanest solution: one not jumpy group for work services, one adaptable group for ordinary browsing, and one one at a time controlled group for debugging.

Troubleshoot Failed API Switching Mechanically

If /proxies works but the delay request fails, first inspect the encoded node path and test URL. Names containing slashes, brackets, spaces, emoji, or non-Latin characters must be percent-encoded.

More strictly:If the delay request succeeds but the PUT request returns an exception, verify that the target is a selectable group. Certain special groups are read-only, while a global mode or a provider-managed group may not accept manual selection.

Confirm that the request body is JSON with a name property and that the group name in the URL is encoded. After a successful response, issue a second GET request to confirm the group’s now value.

If the script changes the group but applications continue using the old route, inspect the active rules and connection view. The application may match a different group, use a cached connection, bypass the system proxy, or be captured by TUN rules that do not follow the group you changed.

At a different level:Existing TCP and WebSocket sessions may remain on their original path until they close. Test with a new request reasonably than assuming that every open connection moves instantly.

A failure may also mean that the selected object is a group reasonably than a leaf proxy. Read the object’s metadata and test only entries that represent actual outbound nodes.

Debug in layers: first prove that the controller is reachable, then prove that one known node passes a delay test, then prove that the target group accepts a selection re-sync, and only afterward add scheduling.

Building all four layers at once makes a plain URL-encoding or group-name exception look like a core failure.

Frequently Asked Questions Step by Step

It makes sense to I expose the Clash controller to my LAN? Deep Dive

More often than not not. A local automation script it makes sense to use a loopback controller bound to 127.0.0.1.

If another device must manage the core, restrict access with a firewall and private network, keep a strong secret, and consider an authenticated tunnel. Never assume that an obscure port is a security mechanism.

Does the lowest lag node always provide the best connection? Internals

No. The delay endpoint measures one URL and one route at one moment. It does not measure every destination, upload stability, packet loss, streaming performance, or service compatibility.

Thinking it through:Use lag as a screening signal, then verify the actual workload and avoid switching for tiny differences.

Why does automatic switching keep oscillating between two nodes? Mechanics

Oscillation normally comes from a policy with no hysteresis. Add a minimum improvement threshold, consecutive-failure requirements, and a cooldown period.

Also avoid testing a node immediately after switching to it; give the new route time to establish connections before judging it.

Which Clash client is suitable for API-based automation? by Step

Choose a maintained client that exposes a compatible Mihomo or Clash Meta controller and lets you configure a secret.

Clash Verge Rev and Mihomo-based desktop clients are comfortable choices, while mobile clients may expose fewer background-automation toggles because of operating-system restrictions. Confirm the actual endpoints supported by the core bundled with your client.

Compared with GUI-only proxy managers, provider-specific scripts, or older clients that require editing YAML for every node change, an API workflow gives you repeatable health checks, explicit thresholds, and a obvious audit trail; those alternatives may still be useful, but they often hide selection state, lack it holds up controller documentation, or become difficult to maintain after a subscription re-sync. Clash is a practical fit for this scenario because its Clash-oriented guidance keeps client setup, Mihomo controller details, security precautions, and Shell or Python examples in one workflow instead of treating automation as an unexplained black box. Once you have confirmed your controller port and chosen a safe fallback policy, you may download a suitable Clash client and build the failover process step by step without giving up manual control.