A list of proxy nodes is inventory. It tells you what exits are available, where they terminate and what protocol each one speaks. What it does not tell you is which one a given connection should actually use at half past nine on a Tuesday when one of them has started dropping packets.
That decision belongs to policy groups. They sit between the rule list and the raw nodes, and they are the part of a configuration that turns a static inventory into something that reacts to conditions. A rule says “send Steam traffic to the group named Gaming”; the group decides, at that moment, which of the six nodes inside it is the one Steam gets.
This article works through the four group types you will meet most often, the YAML fields that control them, and the structural patterns that keep a large configuration readable. The examples are written for the Mihomo-family syntax used by current desktop clients.
The Shape of a Group
Every group is an entry under the top-level proxy-groups key. The two fields present in all of them are name and type. Everything else depends on which type you picked.
proxy-groups:
- name: "PROXY"
type: select
proxies:
- "Tokyo 01"
- "Singapore 02"
- DIRECT
Two details matter there. The group’s name can be used anywhere a node name can, including inside another group. And DIRECT and REJECT are valid members, so a group can offer “no proxy at all” as one of its options.
The fields that appear across the automatic types are:
proxies— an explicit, ordered list of member names.use— pull members from one or more proxy providers instead of listing them by hand.filter— a regular expression applied to member names, so a group can select “everything whose name contains JP” from a subscription.url— the endpoint used for health checks.interval— how often, in seconds, that check runs.timeout— how long a single check may take, in milliseconds, before it counts as a failure.lazy— when true, suspend checking for a group nothing is currently using.tolerance— the latency margin, in milliseconds, before a switch is allowed.strategy— for load balancing only, how connections are spread across members.
The Four Types
select — you decide
The manual type. It exposes its members in the client interface and routes everything to whichever one you have highlighted. No measurement happens unless you ask for it.
It belongs at the top of a configuration, where a person is making a deliberate policy call, and in small feature switches: a group of Auto, DIRECT and REJECT attached to one rule set is a toggle you can flip without editing YAML.
url-test — the fastest wins
Periodically requests url through every member, records the round-trip time, and routes to whichever came back quickest.
- name: "Auto"
type: url-test
proxies:
- "Tokyo 01"
- "Osaka 03"
- "Singapore 02"
url: "http://www.gstatic.com/generate_204"
interval: 300
timeout: 5000
tolerance: 60
lazy: true
The conventional target is a small endpoint returning HTTP 204 with an empty body. You are measuring the time to complete a request, not downloading anything, so the response should be as close to nothing as possible.
fallback — order, not speed
Same health checking, different decision. fallback ignores which member is fastest and uses the first member in the list that is currently passing its check. If member one recovers, traffic returns to it.
- name: "Stable"
type: fallback
proxies:
- "Primary Line"
- "Backup Line"
- "Emergency Line"
url: "http://www.gstatic.com/generate_204"
interval: 180
This is the type to use when the members are not interchangeable. If one node is a paid low-latency line and another is a slow but unmetered spare, you want a strict preference order, not a race. url-test would happily park you on the spare because it happened to answer forty milliseconds faster.
load-balance — spread the connections
Distributes connections across all healthy members rather than picking one. Useful when a single node has a per-connection bandwidth cap, when you want to reduce the chance that one destination sees your whole session arriving from a single address, or simply to keep several lines warm.
- name: "Spread"
type: load-balance
proxies:
- "Node A"
- "Node B"
- "Node C"
url: "http://www.gstatic.com/generate_204"
interval: 300
strategy: consistent-hashing
Health Checks, Tolerance and Flapping
Latency measurements are noisy. Two nodes in the same data centre will trade places on every test run, and if the group switched every time, you would get a stream of broken sessions for no benefit whatsoever. That behaviour is called flapping, and tolerance exists to stop it.
Tolerance sets a margin. The group only abandons its current member if a challenger beats it by more than that many milliseconds. With tolerance: 60 and a current member at 140 ms, a rival at 100 ms is not enough to trigger a move — it would need to come in under 80 ms. The current choice keeps an advantage it did not earn, and that is exactly the point: stability is worth more than a few milliseconds.
Sensible starting values depend on the spread of your nodes. If everything is in one region, 50 to 100 ms is generous enough to hold steady. If the group mixes continents, a larger tolerance stops a marginal improvement from dragging your session across an ocean mid-download. Debugging a group that will not settle is much easier in a GUI that plots each member’s latency next to its name, and a client such as Clash Verge Rev surfaces per-node test results and the group’s current pick on the same screen, which turns a guessing game into a reading exercise.
interval is a related trade-off: short intervals spot a dead node quickly but generate constant background requests through every member, and five minutes suits a group of ten or more nodes. lazy: true then stops the engine testing groups nothing is routed to.
Load-Balance Strategies and Session Stickiness
The strategy field decides how connections are distributed, and the difference between the options is not cosmetic.
StrategyHow it assignsSame destination, later connectionBest forround-robinNext member in rotation, per connectionLikely a different nodeBulk transfers, many independent requestsconsistent-hashingHash of the destination host picks the memberSame node, as long as membership is stableAnything with a login sessionsticky-sessionsHash keyed on source and destination, held for a windowSame node within the windowMulti-connection app sessions
Stickiness matters because a great many services quietly tie state to the address they saw you arrive from. Web logins, streaming licence checks, banking portals, anything using a short-lived token bound to a client IP — all of these can log you out, throw a verification challenge or simply fail if consecutive requests appear to come from three different countries. Round-robin makes that scenario routine.
The rule of thumb: use consistent-hashing unless you have a specific reason not to. Reach for round-robin only for traffic where each connection is genuinely independent — package mirrors, image fetches, download managers pulling parallel chunks. And keep load balancing off your default group entirely; it belongs behind a rule that targets the specific traffic you want spread.
If you would rather start from a working example than a blank file, the setup guide published alongside the clash desktop client walks through a group layout with these strategies already filled in, which is a faster way in than deriving sensible defaults yourself.
Nesting: Region Groups Feeding a Master Group
A group can contain another group. This one fact is what separates a configuration you can maintain from a wall of node names, because it lets you build a small hierarchy instead of one enormous flat list.
The pattern is straightforward. Build one automatic group per region, each filtering members out of your subscription by name. Then build a single manual group whose members are those region groups.
proxy-groups:
- name: "PROXY"
type: select
proxies:
- "Japan"
- "Singapore"
- "Europe"
- "Stable"
- DIRECT
- name: "Japan"
type: url-test
use: ["main-sub"]
filter: "(?i)japan|jp|tokyo|osaka"
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
lazy: true
- name: "Singapore"
type: url-test
use: ["main-sub"]
filter: "(?i)singapore|sg"
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
lazy: true
Your rules now point at PROXY and nothing else, so switching your whole routing policy from Japan to Europe is one click, and twenty new subscription nodes need no edits at all because the filters absorb them on the next update. That combination of stable rule targets and self-populating groups is what lets a configuration survive contact with a subscription that changes its node list every week.
Design Patterns for a Clean Layout
A configuration that stays understandable after six months usually follows a few conventions.
- One master group per intent, not per node. Think in categories — general traffic, media, work tools, downloads — and give each a group. Rules reference intents; groups resolve them to nodes.
- Put DIRECT inside every master group. When a service breaks, being able to send it straight out without touching the rule list saves a restart.
- Name groups for what they mean, not what they contain.
Mediaages better thanJP-fast-2. - Keep automatic groups regional. Latency testing across mixed regions produces the flapping described earlier; testing within one region produces a genuine ranking.
- Reserve
fallbackfor asymmetric members. Ordered preference, not measured preference. - Use
filterrather than hand-listing subscription nodes. Anything typed by hand is something you will retype later. - Set
lazy: trueon region groups. Ten groups checking every five minutes is a surprising amount of idle traffic.
Frequently Asked Questions
Why does my url-test group keep choosing a slow node?
Almost always because the health-check target is not representative. If the check URL resolves to a CDN edge close to one node, that node wins every test regardless of how it performs for your actual traffic. Point the check at a stable endpoint that is not itself geographically biased, and remember that a 204 probe measures handshake latency, not throughput.
Can one node belong to several groups?
Yes, and it usually should. Membership is by name, so the same node can appear in a region group, a fallback chain and a load-balance pool simultaneously. Health checks are tracked per group, so a node under test in three groups will be probed by each of them — another argument for lazy.
What happens if every member of a group fails its check?
Behaviour varies slightly by type. A fallback group with nothing healthy will generally hold on the first member and keep trying, so connections through it fail until something recovers. This is the main reason to keep a DIRECT entry available in the master group above it: you retain a working escape hatch that does not require editing the file.
Does a group add latency of its own?
The selection step is a lookup against values already in memory, so its cost is negligible. Health checking runs on its own schedule and does not sit in the path of your connections. What can cost you is a badly tuned interval on many groups at once, which adds background requests rather than per-connection delay.
Closing Notes
Policy groups are where a routing configuration stops being a list and starts being a system. The four types cover distinct needs — manual control, measured speed, ordered preference, distributed load — and mixing them thoughtfully is far more effective than trying to make one type do everything.
If you take one habit away, make it this: keep rules pointing at a small number of well-named master groups, and let nested region groups track which nodes actually exist. Then tune tolerance until the selection stops moving, and leave it alone. A group you never think about is a group that is working.