Auto-Rotation
xrat supports automatic proxy rotation, periodically switching between configs based on a schedule, health checks, or manual triggers.
Overview
Auto-rotation is managed by the daemon supervisor. When enabled, the daemon:
- Periodically tests candidate configs
- Selects the best candidate based on latency
- Atomically disconnects the old session and connects the new one
- Respects cooldown periods to prevent rapid switching
Configuration
Enable and configure rotation in config.toml:
[runtime.rotation]
enabled = false
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
| Field | Description | Default |
|---|---|---|
enabled | Enable scheduled rotation | false |
interval_secs | Rotation interval in seconds | 1800 (30 minutes) |
health_trigger_enabled | Trigger rotation on health check failure | true |
cooldown_secs | Minimum time between rotations | 300 (5 minutes) |
test_concurrency | Concurrent test workers (0 = auto) | 0 |
test_stages | Test stages to run for candidate selection | ["real_delay", "download"] |
Rotation Triggers
Timer Trigger
Scheduled rotation every interval_secs:
interval_secs = 1800 # rotate every 30 minutes
The daemon maintains a timer that fires at the specified interval, triggering rotation to the best available candidate.
Health Check Trigger
Triggered when the active proxy fails a health check:
health_trigger_enabled = true
The daemon monitors proxy health every 15 seconds. If the health check fails (process dead, port unreachable), rotation is triggered immediately.
Manual Trigger
Triggered by the user via CLI:
xrat proxy rotate
Manual rotation bypasses the timer but respects cooldown.
Forced Rotation
Rotate to a specific config:
xrat proxy rotate --config-id 99
Skips candidate selection and rotates to the specified config.
Cooldown Protection
After a rotation, the daemon enforces a cooldown period:
cooldown_secs = 300 # 5 minutes
During cooldown:
- Timer triggers are delayed until cooldown expires
- Health check triggers are suppressed (unless critical)
- Manual triggers are allowed (user override)
Cooldown State
#![allow(unused)]
fn main() {
struct SupervisorState {
cooldown_until: Option<DateTime<Utc>>,
// ...
}
}
When a rotation completes:
#![allow(unused)]
fn main() {
state.cooldown_until = Some(Utc::now() + Duration::from_secs(cooldown_secs));
}
Candidate Selection
When rotating without --config-id, the daemon selects the best candidate:
Step 1: Load Candidates
Query enabled configs from the database:
SELECT * FROM configs
WHERE is_enabled = true
AND is_deleted = false
AND id != <current_config_id>
Step 2: Test Candidates
Run test stages on all candidates concurrently:
test_concurrency = 4 # test 4 configs at once
test_stages = ["real_delay", "download"]
For each candidate:
- Generate a probe config
- Spawn a short-lived Xray process
- Run the specified test stages
- Collect results (latency, throughput, success/failure)
- Terminate the probe process
Step 3: Filter Failures
Exclude configs that failed any test stage:
#![allow(unused)]
fn main() {
let successful = candidates.into_iter()
.filter(|c| c.real_delay_ok && c.download_ok)
.collect();
}
Step 4: Sort by Latency
Sort by real-delay latency (lowest first):
#![allow(unused)]
fn main() {
successful.sort_by_key(|c| c.real_delay_ms);
}
Step 5: Select Top Candidate
Pick the first config from the sorted list:
#![allow(unused)]
fn main() {
let best = successful.first()?;
}
If no candidates pass testing, rotation is skipped.
Rotation Flow
When rotation is triggered:
- Check cooldown β If active, delay or skip
- Select candidate β Run candidate selection (or use
--config-id) - Atomic replace β Disconnect old session, connect new session
- Update state β Record rotation timestamp, trigger type, new config ID
- Reset timer β Schedule next timer-based rotation
Atomic Replace
The replace flow ensures minimal downtime:
#![allow(unused)]
fn main() {
async fn replace_session(old_id: i64, new_config_id: i64) -> Result<()> {
// 1. Start new session
let new_session = connect(new_config_id).await?;
// 2. Wait for new session to be ready
wait_for_ready(new_session.socks_port).await?;
// 3. Stop old session
disconnect(old_id).await?;
Ok(())
}
}
The new session is started before the old session is stopped, ensuring continuous connectivity.
Rotation Status
Check rotation status:
xrat proxy status
Output:
Proxy Rotation Status
βββββββββββββββββββββββββββββββββ
Enabled: yes
Interval: 1800s
Last rotation: 2026-05-28 10:30:00 (manual)
Next rotation: 2026-05-28 11:00:00
Cooldown: 300s (inactive)
Active config: 42 (vless://example.com:443)
JSON Output
xrat proxy status --json
{
"enabled": true,
"interval_secs": 1800,
"last_trigger": "manual",
"last_rotation_at": "2026-05-28T10:30:00Z",
"next_rotation_at": "2026-05-28T11:00:00Z",
"cooldown_secs": 300,
"cooldown_active": false,
"active_config_id": 42
}
Enabling Rotation
Start Rotation
xrat proxy start
Sends a ProxyStart request to the daemon, which enables the rotation
scheduler.
Stop Rotation
xrat proxy stop
Sends a ProxyStop request to the daemon, which disables the rotation
scheduler. The active proxy session continues running.
Rotation Strategies
Conservative Strategy
Long intervals, strict testing, long cooldown:
[runtime.rotation]
enabled = true
interval_secs = 3600 # 1 hour
health_trigger_enabled = true
cooldown_secs = 600 # 10 minutes
test_stages = ["real_delay", "download"]
Best for: stable connections, minimal disruption
Aggressive Strategy
Short intervals, fast testing, short cooldown:
[runtime.rotation]
enabled = true
interval_secs = 300 # 5 minutes
health_trigger_enabled = true
cooldown_secs = 60 # 1 minute
test_stages = ["real_delay"]
Best for: finding the fastest proxy, frequent optimization
Health-Only Strategy
No scheduled rotation, only rotate on failure:
[runtime.rotation]
enabled = true
interval_secs = 86400 # 24 hours (effectively disabled)
health_trigger_enabled = true
cooldown_secs = 300
test_stages = ["real_delay"]
Best for: stable connections with automatic failover
Persistence
Rotation state is tracked in memory (not persisted to database). On daemon restart:
- Rotation is disabled (must be re-enabled with
xrat proxy start) - Cooldown is reset
- Timer is reset
The active session is persisted and reattached on daemon restart.
Troubleshooting
Rotation Not Triggering
Symptom: Timer fires but rotation doesnβt happen
Check:
- Is cooldown active?
xrat proxy status - Are there enabled configs?
xrat list configs --enabled-only - Do candidates pass testing?
xrat test --enabled-only
Rotation Fails
Symptom: Rotation triggers but new session fails to connect
Check:
- Test the target config manually:
xrat test <id> - Check daemon logs for errors
- Verify Xray binary is available
Rapid Rotation
Symptom: Proxy switches too frequently
Fix: Increase cooldown:
cooldown_secs = 600 # 10 minutes
Or disable health trigger:
health_trigger_enabled = false
Related
proxyCLI β command reference- Daemon and IPC β daemon supervisor
- Testing β test stages used for candidate selection
- Runtime Management β session lifecycle